diff --git a/.coveragerc b/.coveragerc index 96ad6b09b..74dccbfe1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] omit = scripts/* + freqtrade/templates/* freqtrade/vendor/* freqtrade/__main__.py tests/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..f6a111944 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,235 @@ +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 TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"} + export CI_BRANCH=${GITHUB_REF#"ref/heads"} + echo "${TRAVIS_BRANCH}" + coveralls || true + + - name: Backtesting + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy + + - name: Hyperopt + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + 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.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI ${{ matrix.os }}*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + + 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 create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy + + - name: Hyperopt + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + 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.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI windows*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + + 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.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + with: + type: ${{ job.status }} + job_name: '*Freqtrade Docs*' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + + 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.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI Deploy*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml new file mode 100644 index 000000000..57a7e591e --- /dev/null +++ b/.github/workflows/docker_update_readme.yml @@ -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 + diff --git a/.travis.yml b/.travis.yml index 405228ab8..ec688a1f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,31 +24,34 @@ 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 create-userdir --userdir user_data + - freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy name: backtest - script: - cp config.json.example config.json - - freqtrade --datadir tests/testdata hyperopt -e 5 + - freqtrade create-userdir --userdir user_data + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt name: hyperopt - script: flake8 name: flake8 - script: # Test Documentation boxes - # !!! : is not allowed! - - grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0 + # !!! "title" - Title needs to be quoted! + - grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0 name: doc syntax - script: mypy freqtrade scripts name: mypy - - 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: diff --git a/Dockerfile b/Dockerfile index 21432f89f..dc9b04403 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] diff --git a/Dockerfile.pi b/Dockerfile.pi index 85ba5892f..279f85a04 100644 --- a/Dockerfile.pi +++ b/Dockerfile.pi @@ -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" ] diff --git a/README.md b/README.md index 6d57dcd89..a1feeab67 100644 --- a/README.md +++ b/README.md @@ -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/"` 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 diff --git a/build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl new file mode 100644 index 000000000..87469a199 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl differ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 new file mode 100644 index 000000000..30427c3cc --- /dev/null +++ b/build_helpers/install_windows.ps1 @@ -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 . diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 839ca0876..7fe4b17eb 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -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" diff --git a/config.json.example b/config.json.example index 419019030..a2add358f 100644 --- a/config.json.example +++ b/config.json.example @@ -44,7 +44,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], @@ -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" }, diff --git a/config_binance.json.example b/config_binance.json.example index 58817a78e..7d616fe91 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -54,6 +54,9 @@ "BNB/BTC" ] }, + "pairlists": [ + {"method": "StaticPairList"} + ], "edge": { "enabled": false, "process_throttle_secs": 3600, diff --git a/config_full.json.example b/config_full.json.example index 5789e49ac..b9631f63d 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -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, @@ -78,7 +82,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/config_kraken.json.example b/config_kraken.json.example index 5a36941d8..854aeef71 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -46,6 +46,9 @@ ] }, + "pairlists": [ + {"method": "StaticPairList"} + ], "edge": { "enabled": false, "process_throttle_secs": 3600, diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index c50527c8b..5890ae6ab 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -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 diff --git a/docs/backtesting.md b/docs/backtesting.md index 34c5f1fbe..19814303b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -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. @@ -72,6 +72,8 @@ The exported trades can be used for [further analysis](#further-backtest-result- freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json ``` +Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). + #### Supplying custom fee value Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index a0437976f..25818aea6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -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 @@ -73,22 +89,22 @@ The bot allows you to use multiple configuration files by specifying multiple defined in the latter configuration files override parameters with the same name defined in the previous configuration files specified in the command line earlier. -For example, you can make a separate configuration file with your key and secrete +For example, you can make a separate configuration file with your key and secret for the Exchange you use for trading, specify default configuration file with -empty key and secrete values while running in the Dry Mode (which does not actually +empty key and secret values while running in the Dry Mode (which does not actually 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 +This could help you hide your private Exchange key and Exchange secret on you local machine by setting appropriate file permissions for the file which contains actual secrets and, additionally, prevent unintended disclosure of sensitive private data when you publish examples of your configuration in the project issues or in the Internet. @@ -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). diff --git a/docs/configuration.md b/docs/configuration.md index ff40b1750..024760fb9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -38,85 +38,92 @@ The prevelance for all Options is as follows: Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. -| Command | Default | Description | -|----------|---------|-------------| -| `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) -| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. -| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. -| `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. -| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). -| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. -| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. -| `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. -| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). -| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). -| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop` | false | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive` | 0 | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_stop_positive_offset` | 0 | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `trailing_only_offset_is_reached` | false | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). -| `unfilledtimeout.buy` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. -| `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. -| `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). -| `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids. -| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. -| `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book. -| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. -| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks. -| `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. -| `ask_strategy.use_sell_signal` | true | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). -| `ask_strategy.sell_profit_only` | false | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). -| `ask_strategy.ignore_roi_if_buy_signal` | false | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). -| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). -| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). -| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). -| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. -| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.*** -| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). -| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). -| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) -| `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). -| `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.*** -| `webhook.enabled` | false | Enable usage of Webhook notifications -| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. -| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhooksell` | false | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `webhook.webhookstatus` | false | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. -| `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_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. -| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. -| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. -| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`. +| Command | Description | +|----------|-------------| +| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* +| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* +| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
***Datatype:*** *Positive Float as ratio.* +| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* +| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
***Datatype:*** *String* +| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
***Datatype:*** *Boolean* +| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* +| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* +| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float (as ratio)* +| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Boolean* +| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float* +| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
***Datatype:*** *Float* +| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
***Datatype:*** *Integer* +| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
***Datatype:*** *Integer* +| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). +| `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids.
***Datatype:*** *Boolean* +| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.*
***Datatype:*** *Positive Integer* +| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book.
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.*
***Datatype:*** *Float (as ratio)* +| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks.
***Datatype:*** *Boolean* +| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
***Datatype:*** *Positive Integer* +| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
***Datatype:*** *Positive Integer* +| `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
***Datatype:*** *Boolean* +| `ask_strategy.sell_profit_only` | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `ask_strategy.ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* +| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* +| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
***Datatype:*** *String* +| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
***Datatype:*** *Boolean* +| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)).
***Datatype:*** *List* +| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)).
***Datatype:*** *List* +| `exchange.ccxt_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
***Datatype:*** *Dict* +| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
***Datatype:*** *Dict* +| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
***Datatype:*** *Positive Integer* +| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. +| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
***Datatype:*** *Boolean* +| `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
*Defaults to `StaticPairList`.*
***Datatype:*** *List of Dicts* +| `telegram.enabled` | Enable the usage of Telegram.
***Datatype:*** *Boolean* +| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `webhook.enabled` | Enable usage of Webhook notifications
***Datatype:*** *Boolean* +| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* +| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Boolean* +| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *IPv4* +| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* +| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
***Datatype:*** *String, SQLAlchemy connect string* +| `initial_state` | Defines the initial application state. More information below.
*Defaults to `stopped`.*
***Datatype:*** *Enum, either `stopped` or `running`* +| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
***Datatype:*** *Boolean* +| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
***Datatype:*** *ClassName* +| `strategy_path` | Adds an additional strategy lookup path (must be a directory).
***Datatype:*** *String* +| `internals.process_throttle_secs` | Set the process throttle. Value in second.
*Defaults to `5` seconds.*
***Datatype:*** *Positive Integer* +| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
*Defaults to `60` seconds.*
***Datatype:*** *Positive Integer or 0* +| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
***Datatype:*** *Boolean* +| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file.
***Datatype:*** *String* +| `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
***Datatype:*** *String* ### Parameters in the strategy The following parameters can be set in either configuration file or strategy. Values set in the configuration file always overwrite values set in the strategy. -* `ticker_interval` * `minimal_roi` +* `ticker_interval` * `stoploss` * `trailing_stop` * `trailing_stop_positive` * `trailing_stop_positive_offset` +* `trailing_only_offset_is_reached` * `process_only_new_candles` * `order_types` * `order_time_in_force` +* `stake_currency` +* `stake_amount` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) @@ -124,18 +131,22 @@ Values set in the configuration file always overwrite values set in the strategy ### Understand stake_amount The `stake_amount` configuration parameter is an amount of crypto-currency your bot will use for each trade. -The minimal value is 0.0005. If there is not enough crypto-currency in -the account an exception is generated. + +The minimal configuration value is 0.0001. Please check your exchange's trading minimums to avoid problems. + +This setting works in combination with `max_open_trades`. The maximum capital engaged in trades is `stake_amount * max_open_trades`. +For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`. + To allow the bot to trade all the available `stake_currency` in your account set ```json "stake_amount" : "unlimited", ``` -In this case a trade amount is calclulated as: +In this case a trade amount is calculated as: ```python -currency_balanse / (max_open_trades - current_open_trades) +currency_balance / (max_open_trades - current_open_trades) ``` ### Understand minimal_roi @@ -215,6 +226,11 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails. The below is the default which is used if this is not configured in either strategy or configuration file. +Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. +`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%. +Calculation example: we bought the asset at 100$. +Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. + Syntax for Strategy: ```python @@ -224,7 +240,8 @@ order_types = { "emergencysell": "market", "stoploss": "market", "stoploss_on_exchange": False, - "stoploss_on_exchange_interval": 60 + "stoploss_on_exchange_interval": 60, + "stoploss_on_exchange_limit_ratio": 0.99, } ``` @@ -254,7 +271,7 @@ Configuration: !!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. -!!! Warning stoploss_on_exchange failures +!!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. ### Understand order_time_in_force @@ -351,13 +368,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 @@ -377,6 +387,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 @@ -392,7 +484,7 @@ creating trades on the exchange. "db_url": "sqlite:///tradesv3.dryrun.sqlite", ``` -3. Remove your Exchange API key and secrete (change them by empty values or fake credentials): +3. Remove your Exchange API key and secret (change them by empty values or fake credentials): ```json "exchange": { @@ -406,39 +498,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). - -Example: - -```json -"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 @@ -464,12 +523,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. @@ -489,14 +550,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 diff --git a/docs/data-download.md b/docs/data-download.md index bf4792ea3..1f03b124a 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"` Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. -!!! Tip Updating existing data +!!! Tip "Tip: Updating existing data" If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. @@ -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 diff --git a/docs/developer.md b/docs/developer.md index 391493b09..d731f1768 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -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) @@ -194,24 +200,38 @@ If the day shows the same day, then the last candle can be assumed as incomplete To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook. ``` bash -jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/notebooks/strategy_analysis_example.ipynb -jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md +jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/templates/strategy_analysis_example.ipynb +jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/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. ### Create release branch -``` bash -# make sure you're in develop branch -git checkout develop +First, pick a commit that's about one week old (to not include latest additions to releases). +``` bash # create new branch -git checkout -b new_release +git checkout -b new_release ``` +Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. + * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Commit this part * push that branch to the remote and create a PR against the master branch @@ -219,23 +239,18 @@ git checkout -b new_release ### Create changelog from git commits !!! Note - Make sure that both master and develop are up-todate!. + Make sure that the master branch is uptodate! ``` bash # Needs to be done before merging / pulling that branch. -git log --oneline --no-decorate --no-merges master..develop +git log --oneline --no-decorate --no-merges master..new_release ``` ### Create github release / tag Once the PR against master is merged (best right after merging): -* Use the button "Draft a new release" in the Github UI (subsection releases) +* Use the button "Draft a new release" in the Github UI (subsection releases). * Use the version-number specified as tag. * Use "master" as reference (this step comes after the above PR is merged). -* Use the above changelog as release comment (as codeblock) - -### After-release - -* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). -* Create a PR against develop to update that branch. +* Use the above changelog as release comment (as codeblock). diff --git a/docs/docker.md b/docs/docker.md index 923dec1e2..ff5bf7f25 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -26,7 +26,7 @@ To update the image, simply run the above commands again and restart your runnin Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). -!!! Note Docker image update frequency +!!! Note "Docker image update frequency" The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. @@ -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 @@ -170,6 +170,9 @@ docker run -d \ !!! Note All available bot command line parameters can be added to the end of the `docker run` command. +!!! Note + You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + ### Monitor your Docker instance You can use the following commands to monitor and manage your container: @@ -199,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. diff --git a/docs/edge.md b/docs/edge.md index 6374da9e6..c7b088476 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -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 diff --git a/docs/exchanges.md b/docs/exchanges.md new file mode 100644 index 000000000..5bd283a69 --- /dev/null +++ b/docs/exchanges.md @@ -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/"` 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 +``` diff --git a/docs/faq.md b/docs/faq.md index dd92d310e..2416beae4 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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,46 @@ 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? + +By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility subcommands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. + +* In unix shells, this normally can be done as simple as: +```shell +$ freqtrade --some-options 2>&1 >/dev/null | grep 'something' +``` +(note, `2>&1` and `>/dev/null` should be written in this order) + +* Bash interpreter also supports so called process substitution syntax, you can grep the log for a string with it as: +```shell +$ freqtrade --some-options 2> >(grep 'something') >/dev/null +``` +or +```shell +$ freqtrade --some-options 2> >(grep -v 'something' 1>&2) +``` + +* You can also write the copy of Freqtrade log messages to a file with the `--logfile` option: +```shell +$ freqtrade --logfile /path/to/mylogfile.log --some-options +``` +and then grep it as: +```shell +$ cat /path/to/mylogfile.log | grep 'something' +``` +or even on the fly, as the bot works and the logfile grows: +```shell +$ tail -f /path/to/mylogfile.log | grep 'something' +``` +from a separate terminal window. + +On Windows, the `--logfilename` option is also supported by Freqtrade and you can use the `findstr` command to search the log for the string of interest: +``` +> type \path\to\mylogfile.log | findstr "something" +``` ## Hyperopt module diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 99331707f..5a3ae7e3a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -15,25 +15,34 @@ To learn how to get data for the pairs and exchange you're interrested in, head ## Prepare Hyperopting Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py). +the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. +The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`. +This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + ### Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: -* fill `populate_indicators` - probably a copy from your strategy * fill `buy_strategy_generator` - for buy signal optimization * fill `indicator_space` - for buy signal optimzation * fill `sell_strategy_generator` - for sell signal optimization * fill `sell_indicator_space` - for sell signal optimzation -Optional, but recommended: +!!! Note + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. +Optional - can also be loaded from a strategy: + +* copy `populate_indicators` from your strategy - otherwise default-strategy will be used * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used +!!! Note + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + Rarely you may also need to override: * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) @@ -156,7 +165,7 @@ that minimizes the value of the [loss function](#loss-functions). The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to -add it to the `populate_indicators()` method in `hyperopt.py`. +add it to the `populate_indicators()` method in your custom hyperopt file. ## Loss-functions @@ -239,7 +248,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 -e 5000 --spaces all +freqtrade hyperopt --config config.json --hyperopt -e 5000 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -270,6 +279,14 @@ For example, to use one month of data, pass the following parameter to the hyper freqtrade hyperopt --timerange 20180401-20180501 ``` +### Running Hyperopt using methods from a strategy + +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 hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt +``` + ### Running Hyperopt with Smaller Search Space Use the `--spaces` argument to limit the search space used by hyperopt. @@ -341,8 +358,7 @@ So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that t (dataframe['rsi'] < 29.0) ``` -Translating your whole hyperopt result as the new buy-signal -would then look like: +Translating your whole hyperopt result as the new buy-signal would then look like: ```python def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: @@ -410,7 +426,7 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Understand Hyperopt Stoploss results @@ -445,7 +461,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Validate backtesting results diff --git a/docs/installation.md b/docs/installation.md index d45b47b3e..15094232d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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`. ------ @@ -95,29 +103,26 @@ sudo apt-get update sudo apt-get install build-essential git ``` -#### Raspberry Pi / Raspbian +### Raspberry Pi / Raspbian -Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/). +The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. +This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. -The following assumes that miniconda3 is installed and available in your environment. Since the last miniconda3 installation file uses python 3.4, we will update to python 3.6 on this installation. -It's recommended to use (mini)conda for this as installation/compilation of `numpy` and `pandas` takes a long time. - -Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). +Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. ``` bash -conda config --add channels rpi -conda install python=3.6 -conda create -n freqtrade python=3.6 -conda activate freqtrade -conda install pandas numpy +sudo apt-get install python3-venv libatlas-base-dev +git clone https://github.com/freqtrade/freqtrade.git +cd freqtrade -sudo apt install libffi-dev -python3 -m pip install -r requirements-common.txt -python3 -m pip install -e . +bash setup.sh -i ``` +!!! Note "Installation duration" + Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. + !!! Note - This does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. + The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. ### Common @@ -157,7 +162,7 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git - +cd freqtrade ``` Optionally checkout the master branch to get the latest stable release: @@ -166,28 +171,30 @@ Optionally checkout the master branch to get the latest stable release: git checkout master ``` -#### 4. Initialize the configuration - -```bash -cd freqtrade -cp config.json.example config.json -``` - -> *To edit the config please refer to [Bot Configuration](configuration.md).* - -#### 5. Install python dependencies +#### 4. Install python dependencies ``` bash python3 -m pip install --upgrade pip python3 -m pip install -e . ``` +#### 5. Initialize the configuration + +```bash +# Initialize the user_directory +freqtrade create-userdir --userdir user_data/ + +cp config.json.example config.json +``` + +> *To edit the config please refer to [Bot Configuration](configuration.md).* + #### 6. Run the Bot 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. @@ -218,6 +225,12 @@ If that is not available on your system, feel free to try the instructions below ### Install freqtrade manually +!!! Note + Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. + +!!! Hint + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. + #### Clone the git repository ```bash diff --git a/docs/plotting.md b/docs/plotting.md index 89404f8b1..982a5cd65 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -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. ``` @@ -79,11 +103,11 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot. Specify custom indicators. Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). -!!! tip +!!! Tip 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 ` ``` 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. ``` diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 5e7fe7084..bfa5c0d1e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==4.4.3 +mkdocs-material==4.5.0 mdx_truly_sane_lists==1.2 diff --git a/docs/rest-api.md b/docs/rest-api.md index efcacc8a1..187a71c97 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -16,13 +16,20 @@ Sample configuration: }, ``` -!!! Danger Security warning - By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. +!!! Danger "Security warning" + By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger Password selection - Please make sure to select a very strong, unique password to protect your bot from unauthorized access. +!!! 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 [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 diff --git a/docs/stoploss.md b/docs/stoploss.md index f5e2f8df6..105488296 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -3,74 +3,101 @@ The `stoploss` configuration parameter is loss in percentage that should trigger a sale. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. -Most of the strategy files already include the optimal `stoploss` -value. This parameter is optional. If you use it in the configuration file, it will take over the -`stoploss` value from the strategy file. +Most of the strategy files already include the optimal `stoploss` value. -## Stop Loss support +!!! Info + All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. Configuration values will override the strategy values. + +## Stop Loss Types At this stage the bot contains the following stoploss support modes: -1. static stop loss, defined in either the strategy or configuration. -2. trailing stop loss, defined in the configuration. -3. trailing stop loss, custom positive loss, defined in configuration. +1. Static stop loss. +2. Trailing stop loss. +3. Trailing stop loss, custom positive loss. +4. Trailing stop loss only once the trade has reached a certain offset. -!!! Note - All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values. +Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. -Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfuly. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. +In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. -In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. As an example in case of trailing stoploss if the order is on the exchange and the market is going up then the bot automatically cancels the previous stoploss order and put a new one with a stop value higher than previous one. It is clear that the bot cannot do it every 5 seconds otherwise it gets banned. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). +For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. +The bot cannot do this every 5 seconds (at each iteration), otherwise it would get banned by the exchange. +So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). +This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note Stoploss on exchange is only supported for Binance as of now. ## Static Stop Loss -This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which -will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss. +This is very simple, you define a stop loss of x (as a ratio of price, i.e. x * 100% of price). This will try to sell the asset once the loss exceeds the defined loss. ## Trailing Stop Loss -The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally. -To enable this Feauture all you have to do is to define the configuration element: +The initial value for this is `stoploss`, just as you would define your static Stop loss. +To enable trailing stoploss: -``` json -"trailing_stop" : True +``` python +trailing_stop = True ``` -This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases. +This will now activate an algorithm, which automatically moves the stop loss up every time the price of your asset increases. -For example, simplified math, +For example, simplified math: -* you buy an asset at a price of 100$ -* your stop loss is defined at 2% -* which means your stop loss, gets triggered once your asset dropped below 98$ -* assuming your asset now increases to 102$ -* your stop loss, will now be 2% of 102$ or 99.96$ -* now your asset drops in value to 101$, your stop loss, will still be 99.96$ +* the bot buys an asset at a price of 100$ +* the stop loss is defined at 2% +* the stop loss would get triggered once the asset dropps below 98$ +* assuming the asset now increases to 102$ +* the stop loss will now be 2% of 102$ or 99.96$ +* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$. -basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price +In summary: The stoploss will be adjusted to be always be 2% of the highest observed price. -### Custom positive loss +### Custom positive stoploss -Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, -the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit, -it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. +It is also possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, the system will utilize a new stop loss, which can have a different value. +For example your default stop loss is 5%, but once you have 1.1% profit, it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. -Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. +Both values require `trailing_stop` to be set to true. -``` json - "trailing_stop_positive": 0.01, - "trailing_stop_positive_offset": 0.011, - "trailing_only_offset_is_reached": false +``` python + trailing_stop_positive = 0.01 + trailing_stop_positive_offset = 0.011 ``` The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. +Before this, `stoploss` is used for the trailing stoploss. -You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. +Read the [next section](#trailing-only-once-offset-is-reached) to keep stoploss at 5% of the entry point. -If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured`stoploss`. +!!! Tip + Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. + +### Trailing only once offset is reached + +It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns. + +If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. +This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. + +``` python + trailing_stop_positive_offset = 0.011 + trailing_only_offset_is_reached = true +``` + +Simplified example: + +``` python + stoploss = 0.05 + trailing_stop_positive_offset = 0.03 + trailing_only_offset_is_reached = True +``` + +* the bot buys an asset at a price of 100$ +* the stop loss is defined at 5% +* the stop loss will remain at 95% until profit reaches +3% ## Changing stoploss on open trades diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ab7dcfc30..c43d8e3f6 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -7,24 +7,28 @@ indicators. This is very simple. Copy paste your strategy file into the directory `user_data/strategies`. -Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: +Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`: -1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py` +1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py` 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 +## Develop your own strategy -The bot includes a default strategy file. However, we recommend you to -use your own file to not have to lose your parameters every time the default -strategy file will be updated on Github. Put your custom strategy file -into the directory `user_data/strategies`. +The bot includes a default strategy file. +Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). -Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. -`cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py` +You will however most likely have your own idea for a strategy. +This document intends to help you develop one for yourself. + +To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`. +This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`. + +!!! Note + This is just a template file, which will most likely not be profitable out of the box. ### Anatomy of a strategy @@ -45,19 +49,19 @@ 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) +**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py) file as reference.** -!!! Note Strategies and Backtesting +!!! Note "Strategies and Backtesting" To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware that during backtesting the full time-interval is passed to the `populate_*()` methods at once. It is therefore best to use vectorized operations (across the whole dataframe, not loops) and avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle. -!!! Warning Using future data +!!! Warning "Warning: Using future data" Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author needs to take care to avoid having the strategy utilize data from the future. Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document. @@ -114,9 +118,40 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame ``` !!! Note "Want more indicator examples?" - Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). + Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). Then uncomment indicators you need. +### Strategy startup period + +Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. +To account for this, the strategy can be assigned the `startup_candle_count` attribute. +This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. + +In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles. + +``` python + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) +``` + +By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. + +!!! Warning + `startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations. + +#### Example + +Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above. + +``` bash +freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m +``` + +Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. +If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting. + +!!! Note + If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00. + ### Buy signal rules Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy. @@ -267,10 +302,10 @@ class Awesomestrategy(IStrategy): ``` !!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. !!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. ### Additional data (DataProvider) @@ -283,9 +318,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. @@ -296,15 +331,15 @@ 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 +!!! Warning "Warning about backtesting" Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` for the backtesting runmode) provides the full time-range in one go, so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). -!!! Warning Warning in hyperopt +!!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. #### Orderbook @@ -374,6 +409,52 @@ if self.wallets: - `get_used(asset)` - currently tied up balance (open orders) - `get_total(asset)` - total available balance - sum of the 2 above +### Additional data (Trades) + +A history of Trades can be retrieved in the strategy by querying the database. + +At the top of the file, import Trade. + +```python +from freqtrade.persistence import Trade +``` + +The following example queries for the current pair and trades from today, however other filters can easily be added. + +``` python +if self.config['runmode'] in ('live', 'dry_run'): + trades = Trade.get_trades([Trade.pair == metadata['pair'], + Trade.open_date > datetime.utcnow() - timedelta(days=1), + Trade.is_open == False, + ]).order_by(Trade.close_date).all() + # Summarize profit for this pair. + curdayprofit = sum(trade.close_profit for trade in trades) +``` + +Get amount of stake_currency currently invested in Trades: + +``` python +if self.config['runmode'] in ('live', 'dry_run'): + total_stakes = Trade.total_open_trades_stakes() +``` + +Retrieve performance per pair. +Returns a List of dicts per pair. + +``` python +if self.config['runmode'] in ('live', 'dry_run'): + performance = Trade.get_overall_performance() +``` + +Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015). + +``` json +{'pair': "ETH/BTC", 'profit': 0.015, 'count': 5} +``` + +!!! Warning + Trade history is not available during backtesting or hyperopt. + ### Print created dataframe To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. @@ -401,14 +482,14 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i ### Where can i find a strategy template? The strategy template is located in the file -[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). +[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). ### Specify custom strategy location 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 diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 49800bbb3..9e61bda65 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -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 @@ -107,6 +107,22 @@ trades = load_trades_from_db("sqlite:///tradesv3.sqlite") trades.groupby("pair")["sell_reason"].value_counts() ``` +## Analyze the loaded trades for trade parallelism +This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`. + +`analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle. + + +```python +from freqtrade.data.btanalysis import analyze_trade_parallelism + +# Analyze the above +parallel_trades = analyze_trade_parallelism(trades, '5m') + + +parallel_trades.plot() +``` + ## Plot results Freqtrade offers interactive plotting capabilities based on plotly. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index e06d4fdfc..ed0c21a6e 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -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 @@ -93,7 +94,7 @@ Once all positions are sold, run `/stop` to completely stop the bot. `/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. -!!! warning +!!! Warning The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. ### /status diff --git a/docs/utils.md b/docs/utils.md index 9f5792660..ca4b645a5 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -2,6 +2,116 @@ Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section. +## Create userdir + +Creates the directory structure to hold your files for freqtrade. +Will also create strategy and hyperopt examples for you to get started. +Can be used multiple times - using `--reset` will reset the sample strategy and hyperopt files to their default state. + +``` +usage: freqtrade create-userdir [-h] [--userdir PATH] [--reset] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + --reset Reset sample files to their original state. +``` + +!!! Warning + Using `--reset` may result in loss of data, since this will overwrite all sample files without asking again. + +``` +├── backtest_results +├── data +├── hyperopt_results +├── hyperopts +│   ├── sample_hyperopt_advanced.py +│   ├── sample_hyperopt_loss.py +│   └── sample_hyperopt.py +├── notebooks +│   └── strategy_analysis_example.ipynb +├── plot +└── strategies + └── sample_strategy.py +``` + +## Create new strategy + +Creates a new strategy from a template similar to SampleStrategy. +The file will be named inline with your class name, and will not overwrite existing files. + +Results will be located in `user_data/strategies/.py`. + +### Sample usage of new-strategy + +```bash +freqtrade new-strategy --strategy AwesomeStrategy +``` + +With custom user directory + +```bash +freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy +``` + +### new-strategy complete options + +``` output +usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME] + [--template {full,minimal}] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --template {full,minimal} + Use a template which is either `minimal` or `full` + (containing multiple sample indicators). Default: + `full`. + +``` + +## Create new hyperopt + +Creates a new hyperopt from a template similar to SampleHyperopt. +The file will be named inline with your class name, and will not overwrite existing files. + +Results will be located in `user_data/hyperopts/.py`. + +### Sample usage of new-hyperopt + +```bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +With custom user directory + +```bash +freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt +``` + +### new-hyperopt complete options + +``` output +usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME] + [--template {full,minimal}] + +optional arguments: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --template {full,minimal} + Use a template which is either `minimal` or `full` + (containing multiple sample indicators). Default: + `full`. +``` + ## List Exchanges Use the `list-exchanges` subcommand to see the exchanges available for the bot. diff --git a/freqtrade.service b/freqtrade.service index 9de9f13c7..df220ed39 100644 --- a/freqtrade.service +++ b/freqtrade.service @@ -6,7 +6,7 @@ After=network.target # Set WorkingDirectory and ExecStart to your file paths accordingly # NOTE: %h will be resolved to /home/ WorkingDirectory=%h/freqtrade -ExecStart=/usr/bin/freqtrade +ExecStart=/usr/bin/freqtrade trade Restart=on-failure [Install] diff --git a/freqtrade.service.watchdog b/freqtrade.service.watchdog index ba491fa53..66ea00d76 100644 --- a/freqtrade.service.watchdog +++ b/freqtrade.service.watchdog @@ -6,7 +6,7 @@ After=network.target # Set WorkingDirectory and ExecStart to your file paths accordingly # NOTE: %h will be resolved to /home/ WorkingDirectory=%h/freqtrade -ExecStart=/usr/bin/freqtrade --sd-notify +ExecStart=/usr/bin/freqtrade trade --sd-notify Restart=always #Restart=on-failure diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index ac59421a7..63c38d8c5 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,4 +1,5 @@ from freqtrade.configuration.arguments import Arguments # noqa: F401 +from freqtrade.configuration.check_exchange import check_exchange, remove_credentials # noqa: F401 from freqtrade.configuration.timerange import TimeRange # noqa: F401 from freqtrade.configuration.configuration import Configuration # noqa: F401 from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401 diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index b0156fcd1..b23366d7a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -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"] @@ -37,13 +37,18 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] -ARGS_CREATE_USERDIR = ["user_data_dir"] +ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] + +ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] + +ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] 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"] @@ -51,7 +56,7 @@ ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", "plot-dataframe", "plot-profit"] -NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] +NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] class Arguments: @@ -61,11 +66,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 +73,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 +84,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 +105,81 @@ 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_new_hyperopt, start_new_strategy, + 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 new-strategy subcommand + build_strategy_cmd = subparsers.add_parser('new-strategy', + help="Create new strategy") + build_strategy_cmd.set_defaults(func=start_new_strategy) + self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd) + + # add new-hyperopt subcommand + build_hyperopt_cmd = subparsers.add_parser('new-hyperopt', + help="Create new hyperopt") + build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) + self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_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 +187,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 +196,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 +205,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 +214,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 +232,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) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 5e811fb81..c739de692 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -10,6 +10,19 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) +def remove_credentials(config: Dict[str, Any]): + """ + Removes exchange keys from the configuration and specifies dry-run + Used for backtesting / hyperopt / edge and utils. + Modifies the input dict! + """ + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + config['exchange']['password'] = '' + config['exchange']['uid'] = '' + config['dry_run'] = True + + def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: """ Check if the exchange name in the config file is supported by Freqtrade @@ -21,7 +34,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: and thus is not known for the Freqtrade at all. """ - if config['runmode'] in [RunMode.PLOT] and not config.get('exchange', {}).get('name'): + if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE, RunMode.OTHER] + and not config.get('exchange', {}).get('name')): # Skip checking exchange in plot mode, since it requires no exchange return True logger.info("Checking exchange...") diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 4c6608c08..0dae6a608 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -63,12 +63,16 @@ AVAILABLE_CLI_OPTIONS = { help='Path to userdata directory.', metavar='PATH', ), + "reset": Arg( + '--reset', + help='Reset sample files to their original state.', + action='store_true', + ), # 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', @@ -87,6 +91,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', @@ -137,7 +146,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', @@ -157,14 +166,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( @@ -175,7 +183,7 @@ AVAILABLE_CLI_OPTIONS = { default=constants.HYPEROPT_EPOCH, ), "spaces": Arg( - '-s', '--spaces', + '--spaces', help='Specify which parameters to hyperopt. Space-separated list. ' 'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], @@ -332,6 +340,14 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + # Templating options + "template": Arg( + '--template', + help='Use a template which is either `minimal` or ' + '`full` (containing multiple sample indicators). Default: `%(default)s`.', + choices=['full', 'minimal'], + default='full', + ), # Plot dataframe "indicators1": Arg( '--indicators1', diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 6a8374e6d..4bfd24677 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -5,7 +5,7 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import constants, OperationalException - +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -61,9 +61,15 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: :param conf: Config in JSON format :return: Returns None if everything is ok, otherwise throw an OperationalException """ + # validating trailing stoploss _validate_trailing_stoploss(conf) _validate_edge(conf) + _validate_whitelist(conf) + + # validate configuration before returning + logger.info('Validating configuration ...') + validate_config_schema(conf) def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: @@ -111,3 +117,17 @@ def _validate_edge(conf: Dict[str, Any]) -> None: "Edge and VolumePairList are incompatible, " "Edge will override whatever pairs VolumePairlist selects." ) + + +def _validate_whitelist(conf: Dict[str, Any]) -> None: + """ + Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. + """ + if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT, + RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]: + return + + 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.") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 034f8d386..277bf8da9 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -9,15 +9,13 @@ from typing import Any, Callable, Dict, List, Optional from freqtrade import OperationalException, constants from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.config_validation import (validate_config_consistency, - validate_config_schema) from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import (create_datadir, create_userdata_dir) from freqtrade.configuration.load_config import load_config_file from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts, json_load -from freqtrade.state import RunMode +from freqtrade.state import RunMode, TRADING_MODES, NON_UTIL_MODES logger = logging.getLogger(__name__) @@ -81,9 +79,8 @@ class Configuration: if 'ask_strategy' not in config: config['ask_strategy'] = {} - # validate configuration before returning - logger.info('Validating configuration ...') - validate_config_schema(config) + if 'pairlists' not in config: + config['pairlists'] = [] return config @@ -93,19 +90,21 @@ 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) + self._process_runmode(config) + self._process_common_options(config) + self._process_trading_options(config) + self._process_optimize_options(config) self._process_plot_options(config) - self._process_runmode(config) - # Check if the exchange set by the user is supported check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) @@ -113,8 +112,6 @@ class Configuration: process_temporary_deprecated_settings(config) - validate_config_consistency(config) - return config def _process_logging_options(self, config: Dict[str, Any]) -> None: @@ -130,21 +127,9 @@ class Configuration: setup_logging(config) - def _process_common_options(self, config: Dict[str, Any]) -> None: - - 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'): - config.update({'strategy': self.args.get("strategy")}) - - self._args_to_config(config, argname='strategy_path', - logstring='Using additional Strategy lookup path: {}') - - if ('db_url' in self.args and self.args["db_url"] and - self.args["db_url"] != constants.DEFAULT_DB_PROD_URL): - config.update({'db_url': self.args["db_url"]}) - logger.info('Parameter --db-url detected ...') + def _process_trading_options(self, config: Dict[str, Any]) -> None: + if config['runmode'] not in TRADING_MODES: + return if config.get('dry_run', False): logger.info('Dry run is enabled') @@ -158,17 +143,33 @@ class Configuration: logger.info(f'Using DB: "{config["db_url"]}"') + def _process_common_options(self, config: Dict[str, Any]) -> None: + + self._process_logging_options(config) + + # Set strategy if not specified in config and or if it's non default + 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', + logstring='Using additional Strategy lookup path: {}') + + if ('db_url' in self.args and self.args["db_url"] and + self.args["db_url"] != constants.DEFAULT_DB_PROD_URL): + config.update({'db_url': self.args["db_url"]}) + logger.info('Parameter --db-url detected ...') + if config.get('forcebuy_enable', False): logger.warning('`forcebuy` RPC message enabled.') - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') - # Support for sd_notify 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 @@ -179,6 +180,9 @@ class Configuration: config['exchange']['name'] = self.args["exchange"] logger.info(f"Using exchange {config['exchange']['name']}") + if 'pair_whitelist' not in config['exchange']: + config['exchange']['pair_whitelist'] = [] + if 'user_data_dir' in self.args and self.args["user_data_dir"]: config.update({'user_data_dir': self.args["user_data_dir"]}) elif 'user_data_dir' not in config: @@ -209,6 +213,10 @@ class Configuration: self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') + # Setting max_open_trades to infinite if -1 + if config.get('max_open_trades') == -1: + config['max_open_trades'] = float('inf') + if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: config.update({'use_max_market_positions': False}) logger.info('Parameter --disable-max-market-positions detected ...') @@ -217,7 +225,7 @@ class Configuration: config.update({'max_open_trades': self.args["max_open_trades"]}) logger.info('Parameter --max_open_trades detected, ' 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) - else: + elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) self._args_to_config(config, argname='stake_amount', diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index f00b23894..b1e3535a3 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -57,3 +57,26 @@ 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 not config.get('pairlists') and not config.get('pairlists'): + config['pairlists'] = [{'method': 'StaticPairList'}] + logger.warning( + "DEPRECATED: " + "Pairlists must be defined explicitly in the future." + "Defaulting to StaticPairList for now.") + + if config.get('pairlist', {}).get("method") == 'VolumePairList': + logger.warning( + "DEPRECATED: " + f"Using VolumePairList in pairlist is deprecated and must be moved to pairlists. " + "Please refer to the docs on configuration details") + pl = {'method': 'VolumePairList'} + pl.update(config.get('pairlist', {}).get('config')) + config['pairlists'].append(pl) + + if config.get('pairlist', {}).get('config', {}).get('precision_filter'): + logger.warning( + "DEPRECATED: " + f"Using precision_filter setting is deprecated and has been replaced by" + "PrecisionFilter. Please refer to the docs on configuration details") + config['pairlists'].append({'method': 'PrecisionFilter'}) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 395accd90..3dd76a025 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -1,8 +1,10 @@ import logging -from typing import Any, Dict, Optional +import shutil from pathlib import Path +from typing import Any, Dict, Optional from freqtrade import OperationalException +from freqtrade.constants import USER_DATA_FILES logger = logging.getLogger(__name__) @@ -31,7 +33,8 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: :param create_dir: Create directory if it does not exist. :return: Path object containing the directory """ - sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ] + sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "notebooks", + "plot", "strategies", ] folder = Path(directory) if not folder.is_dir(): if create_dir: @@ -48,3 +51,26 @@ def create_userdata_dir(directory: str, create_dir=False) -> Path: if not subfolder.is_dir(): subfolder.mkdir(parents=False) return folder + + +def copy_sample_files(directory: Path, overwrite: bool = False) -> None: + """ + Copy files from templates to User data directory. + :param directory: Directory to copy data to + :param overwrite: Overwrite existing sample files + """ + if not directory.is_dir(): + raise OperationalException(f"Directory `{directory}` does not exist.") + sourcedir = Path(__file__).parents[1] / "templates" + for source, target in USER_DATA_FILES.items(): + targetdir = directory / target + if not targetdir.is_dir(): + raise OperationalException(f"Directory `{targetdir}` does not exist.") + targetfile = targetdir / source + if targetfile.exists(): + if not overwrite: + logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") + continue + else: + logger.warning(f"File `{targetfile}` exists already, overwriting.") + shutil.copy(str(sourcedir / source), str(targetfile)) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index fc759ab6e..a8be873df 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -1,11 +1,14 @@ """ This module contains the argument manager class """ +import logging import re from typing import Optional import arrow +logger = logging.getLogger(__name__) + class TimeRange: """ @@ -27,6 +30,34 @@ class TimeRange: return (self.starttype == other.starttype and self.stoptype == other.stoptype and self.startts == other.startts and self.stopts == other.stopts) + def subtract_start(self, seconds) -> None: + """ + Subtracts from startts if startts is set. + :param seconds: Seconds to subtract from starttime + :return: None (Modifies the object in place) + """ + if self.startts: + self.startts = self.startts - seconds + + def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, + min_date: arrow.Arrow) -> None: + """ + Adjust startts by candles. + Applies only if no startup-candles have been available. + :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 + :return: None (Modifies the object in place) + """ + if (not self.starttype or (startup_candles + and min_date.timestamp >= self.startts)): + # 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 + timeframe_secs * startup_candles) + self.starttype = 'date' + @staticmethod def parse_timerange(text: Optional[str]): """ diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e8f3f5783..f5e5969eb 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -6,11 +6,8 @@ bot constants DEFAULT_CONFIG = 'config.json' DEFAULT_EXCHANGE = 'bittrex' 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 +17,23 @@ 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 = [ +USERPATH_HYPEROPTS = 'hyperopts' +USERPATH_STRATEGY = 'strategies' + +# Soure files with destination directories within user-directory +USER_DATA_FILES = { + 'sample_strategy.py': USERPATH_STRATEGY, + 'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS, + 'sample_hyperopt_loss.py': USERPATH_HYPEROPTS, + 'sample_hyperopt.py': USERPATH_HYPEROPTS, + 'strategy_analysis_example.ipynb': 'notebooks', +} + +TIMEFRAMES = [ '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', @@ -56,13 +65,13 @@ MINIMAL_CONFIG = { CONF_SCHEMA = { 'type': 'object', 'properties': { - 'max_open_trades': {'type': 'integer', 'minimum': -1}, - 'ticker_interval': {'type': 'string', 'enum': TICKER_INTERVALS}, + 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, + 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { - "type": ["number", "string"], - "minimum": 0.0005, - "pattern": UNLIMITED_STAKE_AMOUNT + 'type': ['number', 'string'], + 'minimum': 0.0001, + 'pattern': UNLIMITED_STAKE_AMOUNT }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, @@ -84,8 +93,8 @@ CONF_SCHEMA = { 'unfilledtimeout': { 'type': 'object', 'properties': { - 'buy': {'type': 'number', 'minimum': 3}, - 'sell': {'type': 'number', 'minimum': 10} + 'buy': {'type': 'number', 'minimum': 1}, + 'sell': {'type': 'number', 'minimum': 1} } }, 'bid_strategy': { @@ -97,7 +106,7 @@ CONF_SCHEMA = { 'maximum': 1, 'exclusiveMaximum': False, 'use_order_book': {'type': 'boolean'}, - 'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1}, + 'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1}, 'check_depth_of_market': { 'type': 'object', 'properties': { @@ -113,8 +122,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'use_order_book': {'type': 'boolean'}, - 'order_book_min': {'type': 'number', 'minimum': 1}, - 'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}, + 'order_book_min': {'type': 'integer', 'minimum': 1}, + 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} @@ -151,13 +160,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', @@ -184,8 +196,8 @@ CONF_SCHEMA = { 'listen_ip_address': {'format': 'ipv4'}, 'listen_port': { 'type': 'integer', - "minimum": 1024, - "maximum": 65535 + 'minimum': 1024, + 'maximum': 65535 }, 'username': {'type': 'string'}, 'password': {'type': 'string'}, @@ -198,7 +210,7 @@ CONF_SCHEMA = { 'internals': { 'type': 'object', 'properties': { - 'process_throttle_secs': {'type': 'number'}, + 'process_throttle_secs': {'type': 'integer'}, 'interval': {'type': 'integer'}, 'sd_notify': {'type': 'boolean'}, } @@ -235,37 +247,37 @@ CONF_SCHEMA = { 'ccxt_config': {'type': 'object'}, 'ccxt_async_config': {'type': 'object'} }, - 'required': ['name', 'pair_whitelist'] + 'required': ['name'] }, 'edge': { 'type': 'object', 'properties': { - "enabled": {'type': 'boolean'}, - "process_throttle_secs": {'type': 'integer', 'minimum': 600}, - "calculate_since_number_of_days": {'type': 'integer'}, - "allowed_risk": {'type': 'number'}, - "capital_available_percentage": {'type': 'number'}, - "stoploss_range_min": {'type': 'number'}, - "stoploss_range_max": {'type': 'number'}, - "stoploss_range_step": {'type': 'number'}, - "minimum_winrate": {'type': 'number'}, - "minimum_expectancy": {'type': 'number'}, - "min_trade_number": {'type': 'number'}, - "max_trade_duration_minute": {'type': 'integer'}, - "remove_pumps": {'type': 'boolean'} + 'enabled': {'type': 'boolean'}, + 'process_throttle_secs': {'type': 'integer', 'minimum': 600}, + 'calculate_since_number_of_days': {'type': 'integer'}, + 'allowed_risk': {'type': 'number'}, + 'capital_available_percentage': {'type': 'number'}, + 'stoploss_range_min': {'type': 'number'}, + 'stoploss_range_max': {'type': 'number'}, + 'stoploss_range_step': {'type': 'number'}, + 'minimum_winrate': {'type': 'number'}, + 'minimum_expectancy': {'type': 'number'}, + 'min_trade_number': {'type': 'number'}, + 'max_trade_duration_minute': {'type': 'integer'}, + 'remove_pumps': {'type': 'boolean'} }, 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] } }, - 'anyOf': [ - {'required': ['exchange']} - ], 'required': [ + 'exchange', 'max_open_trades', 'stake_currency', 'stake_amount', 'dry_run', 'bid_strategy', 'unfilledtimeout', + 'stoploss', + 'minimal_roi', ] } diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 17abae3b6..379c80060 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -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 @@ -52,16 +52,18 @@ def load_backtest_data(filename) -> pd.DataFrame: return df -def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int) -> pd.DataFrame: +def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataFrame: """ Find overlapping trades by expanding each trade once per period it was open - and then counting overlaps + and then counting overlaps. :param results: Results Dataframe - can be loaded - :param freq: Frequency used for the backtest - :param max_open_trades: parameter max_open_trades used during backtest run - :return: dataframe with open-counts per time-period in freq + :param timeframe: Timeframe used for backtest + :return: dataframe with open-counts per time-period in timeframe """ - dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) + from freqtrade.exchange import timeframe_to_minutes + timeframe_min = timeframe_to_minutes(timeframe) + dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, + freq=f"{timeframe_min}min")) for row in results[['open_time', 'close_time']].iterrows()] deltas = [len(x) for x in dates] dates = pd.Series(pd.concat(dates).values, name='date') @@ -69,8 +71,23 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int df2 = pd.concat([dates, df2], axis=1) df2 = df2.set_index('date') - df_final = df2.resample(freq)[['pair']].count() - return df_final[df_final['pair'] > max_open_trades] + df_final = df2.resample(f"{timeframe_min}min")[['pair']].count() + df_final = df_final.rename({'pair': 'open_trades'}, axis=1) + return df_final + + +def evaluate_result_multi(results: pd.DataFrame, timeframe: str, + max_open_trades: int) -> pd.DataFrame: + """ + Find overlapping trades by expanding each trade once per period it was open + and then counting overlaps + :param results: Results Dataframe - can be loaded + :param timeframe: Frequency used for the backtest + :param max_open_trades: parameter max_open_trades used during backtest run + :return: dataframe with open-counts per time-period in freq + """ + df_final = analyze_trade_parallelism(results, timeframe) + return df_final[df_final['open_trades'] > max_open_trades] def load_trades_from_db(db_url: str) -> pd.DataFrame: @@ -89,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) @@ -106,7 +123,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: t.stop_loss, t.initial_stop_loss, t.strategy, t.ticker_interval ) - for t in Trade.query.all()], + for t in Trade.get_trades().all()], columns=columns) return trades @@ -150,15 +167,21 @@ def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "c return df_comb -def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: +def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, + timeframe: str) -> pd.DataFrame: """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :param col_name: Column name that will be assigned the results + :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. """ - # Use groupby/sum().cumsum() to avoid errors when multiple trades sold at the same candle. - df[col_name] = trades.groupby('close_time')['profitperc'].sum().cumsum() + from freqtrade.exchange import timeframe_to_minutes + 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 # FFill to get continuous diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 1ef224978..e45dd451e 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -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) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index f0787281a..7b7159145 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -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]]: diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index ed5d80b0e..ec95be874 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -8,7 +8,8 @@ Includes: import logging import operator -from datetime import datetime +from copy import deepcopy +from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -18,7 +19,7 @@ from pandas import DataFrame from freqtrade import OperationalException, misc from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv -from freqtrade.exchange import Exchange, timeframe_to_minutes +from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seconds logger = logging.getLogger(__name__) @@ -49,13 +50,30 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: return tickerlist[start_index:stop_index] -def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str, +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=timezone.utc) + df = df.loc[df[df_date_col] >= start, :] + if timerange.stoptype == 'date': + 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, 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 [] @@ -66,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) @@ -107,18 +125,19 @@ 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, exchange: Optional[Exchange] = None, fill_up_missing: bool = True, - drop_incomplete: bool = True + drop_incomplete: bool = True, + startup_candles: int = 0, ) -> DataFrame: """ 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. @@ -126,65 +145,88 @@ def load_pair_history(pair: str, :param exchange: Exchange object (needed when using "refresh_pairs") :param fill_up_missing: Fill missing values with "No action"-candles :param drop_incomplete: Drop last candle assuming it may be incomplete. + :param startup_candles: Additional candles to load at the start of the period :return: DataFrame with ohlcv data """ + timerange_startup = deepcopy(timerange) + if startup_candles > 0 and timerange_startup: + 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) + pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) if pairdata: - if timerange: - _validate_pairdata(pair, pairdata, timerange) - return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, + if timerange_startup: + _validate_pairdata(pair, pairdata, timerange_startup) + 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, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, + startup_candles: int = 0, + fail_without_data: bool = False ) -> Dict[str, DataFrame]: """ Loads ticker history data for a list of pairs - :return: dict(:) + :param datadir: Path to the data storage location. + :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.) + :param exchange: Exchange object (needed when using "refresh_pairs") + :param timerange: Limit data to be loaded to this timerange + :param fill_up_missing: Fill missing values with "No action"-candles + :param startup_candles: Additional candles to load at the start of the period + :param fail_without_data: Raise OperationalException if no data is found. + :return: dict(:) TODO: refresh_pairs is still used by edge to keep the data uptodate. This should be replaced in the future. Instead, writing the current candles to disk from dataprovider should be implemented, as this would avoid loading ohlcv data twice. exchange and refresh_pairs are then not needed here nor in load_pair_history. """ result: Dict[str, DataFrame] = {} + if startup_candles > 0 and timerange: + 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, - fill_up_missing=fill_up_missing) + fill_up_missing=fill_up_missing, + startup_candles=startup_candles) if hist is not None: result[pair] = hist + + if fail_without_data and not result: + raise OperationalException("No data found. Terminating.") 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 @@ -194,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]]: """ @@ -212,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() @@ -238,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 """ @@ -260,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( @@ -280,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 @@ -305,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 @@ -421,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. @@ -429,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: diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 2655fbc65..afd20cf61 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -97,10 +97,11 @@ 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 + timerange=self._timerange, + startup_candles=self.strategy.startup_candle_count, ) if not data: diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 0948692f1..df18bca02 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,4 +1,5 @@ -from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401 +from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS # noqa: F401 +from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 is_exchange_bad, is_exchange_known_ccxt, @@ -14,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 diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py new file mode 100644 index 000000000..229abe766 --- /dev/null +++ b/freqtrade/exchange/bibox.py @@ -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}} diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py new file mode 100644 index 000000000..ed30b95c7 --- /dev/null +++ b/freqtrade/exchange/common.py @@ -0,0 +1,124 @@ +import logging + +from freqtrade import DependencyException, TemporaryError + +logger = logging.getLogger(__name__) + + +API_RETRY_COUNT = 4 +BAD_EXCHANGES = { + "bitmex": "Various reasons.", + "bitstamp": "Does not provide history. " + "Details in https://github.com/freqtrade/freqtrade/issues/1983", + "hitbtc": "This API cannot be used with Freqtrade. " + "Use `hitbtc2` exchange id to access this exchange.", + **dict.fromkeys([ + 'adara', + 'anxpro', + 'bigone', + 'coinbase', + 'coinexchange', + 'coinmarketcap', + 'lykke', + 'xbtce', + ], "Does not provide timeframes. ccxt fetchOHLCV: False"), + **dict.fromkeys([ + 'bcex', + 'bit2c', + 'bitbay', + 'bitflyer', + 'bitforex', + 'bithumb', + 'bitso', + 'bitstamp1', + 'bl3p', + 'braziliex', + 'btcbox', + 'btcchina', + 'btctradeim', + 'btctradeua', + 'bxinth', + 'chilebit', + 'coincheck', + 'coinegg', + 'coinfalcon', + 'coinfloor', + 'coingi', + 'coinmate', + 'coinone', + 'coinspot', + 'coolcoin', + 'crypton', + 'deribit', + 'exmo', + 'exx', + 'flowbtc', + 'foxbit', + 'fybse', + # 'hitbtc', + 'ice3x', + 'independentreserve', + 'indodax', + 'itbit', + 'lakebtc', + 'latoken', + 'liquid', + 'livecoin', + 'luno', + 'mixcoins', + 'negociecoins', + 'nova', + 'paymium', + 'southxchange', + 'stronghold', + 'surbitcoin', + 'therock', + 'tidex', + 'vaultoro', + 'vbtc', + 'virwox', + 'yobit', + 'zaif', + ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), +} + +MAP_EXCHANGE_CHILDCLASS = { + 'binanceus': 'binance', + 'binanceje': 'binance', +} + + +def retrier_async(f): + async def wrapper(*args, **kwargs): + count = kwargs.pop('count', API_RETRY_COUNT) + try: + return await f(*args, **kwargs) + except (TemporaryError, DependencyException) as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + return await wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper + + +def retrier(f): + def wrapper(*args, **kwargs): + count = kwargs.pop('count', API_RETRY_COUNT) + try: + return f(*args, **kwargs) + except (TemporaryError, DependencyException) as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + return wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 71f0737ef..30868df07 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,141 +14,25 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async -from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN +from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError, constants) from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts - logger = logging.getLogger(__name__) -API_RETRY_COUNT = 4 -BAD_EXCHANGES = { - "bitmex": "Various reasons.", - "bitstamp": "Does not provide history. " - "Details in https://github.com/freqtrade/freqtrade/issues/1983", - "hitbtc": "This API cannot be used with Freqtrade. " - "Use `hitbtc2` exchange id to access this exchange.", - **dict.fromkeys([ - 'adara', - 'anxpro', - 'bigone', - 'coinbase', - 'coinexchange', - 'coinmarketcap', - 'lykke', - 'xbtce', - ], "Does not provide timeframes. ccxt fetchOHLCV: False"), - **dict.fromkeys([ - 'bcex', - 'bit2c', - 'bitbay', - 'bitflyer', - 'bitforex', - 'bithumb', - 'bitso', - 'bitstamp1', - 'bl3p', - 'braziliex', - 'btcbox', - 'btcchina', - 'btctradeim', - 'btctradeua', - 'bxinth', - 'chilebit', - 'coincheck', - 'coinegg', - 'coinfalcon', - 'coinfloor', - 'coingi', - 'coinmate', - 'coinone', - 'coinspot', - 'coolcoin', - 'crypton', - 'deribit', - 'exmo', - 'exx', - 'flowbtc', - 'foxbit', - 'fybse', - # 'hitbtc', - 'ice3x', - 'independentreserve', - 'indodax', - 'itbit', - 'lakebtc', - 'latoken', - 'liquid', - 'livecoin', - 'luno', - 'mixcoins', - 'negociecoins', - 'nova', - 'paymium', - 'southxchange', - 'stronghold', - 'surbitcoin', - 'therock', - 'tidex', - 'vaultoro', - 'vbtc', - 'virwox', - 'yobit', - 'zaif', - ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), - } - -MAP_EXCHANGE_CHILDCLASS = { - 'binanceus': 'binance', - 'binanceje': 'binance', -} - - -def retrier_async(f): - async def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return await f(*args, **kwargs) - except (TemporaryError, DependencyException) as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - return await wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper - - -def retrier(f): - def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return f(*args, **kwargs) - except (TemporaryError, DependencyException) as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - return wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper - - 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 = {} @@ -210,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) @@ -228,6 +119,7 @@ class Exchange: self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) + self.validate_required_startup_candles(config.get('startup_candle_count', 0)) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -443,6 +335,16 @@ class Exchange: raise OperationalException( f'Time in force policies are not supported for {self.name} yet.') + def validate_required_startup_candles(self, startup_candles) -> None: + """ + Checks if required startup_candles is more than ohlcv_candle_limit. + Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. + """ + if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']: + raise OperationalException( + f"This strategy requires {startup_candles} candles to start. " + f"{self.name} only provides {self._ft_has['ohlcv_candle_limit']}.") + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. @@ -644,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" @@ -697,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( @@ -716,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. @@ -761,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( @@ -910,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) @@ -983,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'): @@ -990,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 @@ -1049,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: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7251715a7..ec341ff0a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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,14 +70,13 @@ 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 \ self.config.get('edge', {}).get('enabled', False) else None - self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] + self.active_pair_whitelist = self._refresh_whitelist() persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) @@ -123,21 +122,10 @@ class FreqtradeBot: # Check whether markets have to be reloaded self.exchange._reload_markets() - # Refresh whitelist - self.pairlists.refresh_pairlist() - self.active_pair_whitelist = self.pairlists.whitelist - - # Calculating Edge positioning - if self.edge: - self.edge.calculate() - self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) - # Query trades from persistence layer trades = Trade.get_open_trades() - # Extend active-pair whitelist with pairs from open trades - # It ensures that tickers are downloaded for open trades - self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) + self.active_pair_whitelist = self._refresh_whitelist(trades) # Refreshing candles self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), @@ -150,21 +138,33 @@ 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)): + and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)): logger.info(f"Bot heartbeat. PID={getpid()}") self._heartbeat_msg = arrow.utcnow().timestamp - def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): + def _refresh_whitelist(self, trades: List[Trade] = []) -> List[str]: """ - Extend whitelist with pairs from open trades + Refresh whitelist from pairlist or edge and extend it with trades. """ - whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist]) + # Refresh whitelist + self.pairlists.refresh_pairlist() + _whitelist = self.pairlists.whitelist + + # Calculating Edge positioning + if self.edge: + self.edge.calculate() + _whitelist = self.edge.adjust(_whitelist) + + if trades: + # Extend active-pair whitelist with pairs from open trades + # It ensures that tickers are downloaded for open trades + _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) + return _whitelist def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]: """ @@ -266,7 +266,11 @@ class FreqtradeBot: amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% amount_reserve_percent = max(amount_reserve_percent, 0.5) - return min(min_stake_amounts) / amount_reserve_percent + + # The value returned should satisfy both limits: for amount (base currency) and + # for cost (quote, stake currency), so max() is used here. + # See also #2575 at github. + return max(min_stake_amounts) / amount_reserve_percent def create_trades(self) -> bool: """ @@ -317,8 +321,7 @@ class FreqtradeBot: (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): buycount += self.execute_buy(_pair, stake_amount) - else: - continue + continue buycount += self.execute_buy(_pair, stake_amount) @@ -632,8 +635,8 @@ class FreqtradeBot: Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. :return: True if the order succeeded, and False in case of problems. """ - # Limit price threshold: As limit price should always be below price - LIMIT_PRICE_PCT = 0.99 + # Limit price threshold: As limit price should always be below stop-price + LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99) try: stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount, @@ -755,23 +758,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.query.filter(Trade.open_order_id.isnot(None)).all(): + 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) @@ -781,23 +789,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() @@ -812,7 +817,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" @@ -823,18 +829,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 diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index a98da25c2..27f16ecc3 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -36,8 +36,8 @@ def setup_logging(config: Dict[str, Any]) -> None: # Log level verbosity = config['verbosity'] - # Log to stdout, not stderr - log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] + # Log to stderr + log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stderr)] logfile = config.get('logfile') if logfile: diff --git a/freqtrade/main.py b/freqtrade/main.py index 4d6f0dce7..7afaeb1a2 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -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 --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) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7682b5285..bcba78cf0 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -127,3 +127,16 @@ def round_dict(d, n): def plural(num, singular: str, plural: str = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' + + +def render_template(templatefile: str, arguments: dict = {}): + + from jinja2 import Environment, PackageLoader, select_autoescape + + env = Environment( + loader=PackageLoader('freqtrade', 'templates'), + autoescape=select_autoescape(['html', 'xml']) + ) + template = env.get_template(templatefile) + + return template.render(**arguments) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 3adf5eb43..1f2f588ef 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -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 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fb8c182ee..d9fb1f2d1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,18 +10,19 @@ from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame +from tabulate import tabulate from freqtrade import OperationalException -from freqtrade.configuration import TimeRange +from freqtrade.configuration import (TimeRange, remove_credentials, + validate_config_consistency) from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import IStrategy, SellType -from tabulate import tabulate logger = logging.getLogger(__name__) @@ -57,11 +58,7 @@ class Backtesting: self.config = config # Reset keys for backtesting - self.config['exchange']['key'] = '' - self.config['exchange']['secret'] = '' - self.config['exchange']['password'] = '' - self.config['exchange']['uid'] = '' - self.config['dry_run'] = True + remove_credentials(self.config) self.strategylist: List[IStrategy] = [] self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange @@ -79,17 +76,21 @@ class Backtesting: stratconf = deepcopy(self.config) stratconf['strategy'] = strat self.strategylist.append(StrategyResolver(stratconf).strategy) + validate_config_consistency(stratconf) else: # No strategy list specified, only one strategy self.strategylist.append(StrategyResolver(self.config).strategy) + validate_config_consistency(self.config) 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]) # Load one (first) strategy self._set_strategy(self.strategylist[0]) @@ -103,6 +104,31 @@ class Backtesting: # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + def load_bt_data(self): + timerange = TimeRange.parse_timerange(None if self.config.get( + 'timerange') is None else str(self.config.get('timerange'))) + + data = history.load_data( + datadir=Path(self.config['datadir']), + pairs=self.config['exchange']['pair_whitelist'], + timeframe=self.timeframe, + timerange=timerange, + startup_candles=self.required_startup, + fail_without_data=True, + ) + + min_date, max_date = history.get_timeframe(data) + + logger.info( + 'Loading data from %s up to %s (%s days)..', + 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.timeframe), + self.required_startup, min_date) + + return data, timerange + def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame, skip_nan: bool = False) -> str: """ @@ -218,7 +244,8 @@ class Backtesting: ticker: Dict = {} # Create ticker dict for pair, pair_data in processed.items(): - pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + pair_data.loc[:, 'buy'] = 0 # cleanup from previous run + pair_data.loc[:, 'sell'] = 0 # cleanup from previous run ticker_data = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() @@ -351,7 +378,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: @@ -403,7 +430,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: @@ -412,39 +439,18 @@ class Backtesting: :return: None """ data: Dict[str, Any] = {} - pairs = self.config['exchange']['pair_whitelist'] logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - - timerange = TimeRange.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) - data = history.load_data( - datadir=Path(self.config['datadir']), - pairs=pairs, - ticker_interval=self.ticker_interval, - timerange=timerange, - ) - - if not data: - logger.critical("No data found. Terminating.") - return # Use max_open_trades in backtesting, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): max_open_trades = self.config['max_open_trades'] else: logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 + + data, timerange = self.load_bt_data() + all_results = {} - - min_date, max_date = history.get_timeframe(data) - - logger.info( - 'Backtesting with data from %s up to %s (%s days)..', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days - ) - for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) @@ -452,6 +458,15 @@ class Backtesting: # need to reprocess data every time to populate signals preprocessed = self.strategy.tickerdata_to_dataframe(data) + # Trim startup period from analyzed dataframe + for pair, df in preprocessed.items(): + preprocessed[pair] = history.trim_dataframe(df, timerange) + min_date, max_date = history.get_timeframe(preprocessed) + + logger.info( + 'Backtesting with data from %s up to %s (%s days)..', + min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days + ) # Execute backtest and print results all_results[self.strategy.get_strategy_name()] = self.backtest( { diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 1ba6bcc65..a667ebb92 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -4,12 +4,14 @@ This module contains the edge backtesting interface """ import logging -from typing import Dict, Any -from tabulate import tabulate -from freqtrade import constants -from freqtrade.edge import Edge +from typing import Any, Dict -from freqtrade.configuration import TimeRange +from tabulate import tabulate + +from freqtrade import constants +from freqtrade.configuration import (TimeRange, remove_credentials, + validate_config_consistency) +from freqtrade.edge import Edge from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver @@ -29,15 +31,13 @@ class EdgeCli: self.config = config # Reset keys for edge - self.config['exchange']['key'] = '' - self.config['exchange']['secret'] = '' - self.config['exchange']['password'] = '' - self.config['exchange']['uid'] = '' + remove_credentials(self.config) self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - self.config['dry_run'] = True self.exchange = Exchange(self.config) self.strategy = StrategyResolver(self.config).strategy + validate_config_consistency(self.config) + self.edge = Edge(config, self.exchange, self.strategy) # Set refresh_pairs to false for edge-cli (it must be true for edge) self.edge._refresh_pairs = False diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 07258a048..836309a62 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,9 +4,9 @@ This module contains the hyperopt logic """ +import locale import logging import sys - from collections import OrderedDict from operator import itemgetter from pathlib import Path @@ -14,23 +14,22 @@ from pprint import pprint from typing import Any, Dict, List, Optional import rapidjson - -from colorama import init as colorama_init from colorama import Fore, Style -from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count +from colorama import init as colorama_init +from joblib import (Parallel, cpu_count, delayed, dump, load, + wrap_non_picklable_objects) from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension -from freqtrade.configuration import TimeRange -from freqtrade.data.history import load_data, get_timeframe -from freqtrade.misc import round_dict +from freqtrade.data.history import get_timeframe, trim_dataframe +from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver - +from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, + HyperOptResolver) logger = logging.getLogger(__name__) @@ -78,6 +77,8 @@ class Hyperopt: # Previous evaluations self.trials: List = [] + self.num_trials_saved = 0 + # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_indicators'): self.backtesting.strategy.advise_indicators = \ @@ -133,13 +134,18 @@ class Hyperopt: arg_dict = {dim.name: value for dim, value in zip(dimensions, params)} return arg_dict - def save_trials(self) -> None: + def save_trials(self, final: bool = False) -> None: """ Save hyperopt trials to file """ - if self.trials: - logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file) + num_trials = len(self.trials) + if num_trials > self.num_trials_saved: + logger.info(f"Saving {num_trials} {plural(num_trials, 'epoch')}.") dump(self.trials, self.trials_file) + self.num_trials_saved = num_trials + if final: + logger.info(f"{num_trials} {plural(num_trials, 'epoch')} " + f"saved to '{self.trials_file}'.") def read_trials(self) -> List: """ @@ -154,6 +160,12 @@ class Hyperopt: """ Display Best hyperopt result """ + # This is printed when Ctrl+C is pressed quickly, before first epochs have + # a chance to be evaluated. + if not self.trials: + print("No epochs evaluated yet, no best result.") + return + results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] params = best_result['params'] @@ -198,12 +210,20 @@ class Hyperopt: # Also round to 5 digits after the decimal point print(f"Stoploss: {round(params.get('stoploss'), 5)}") + def is_best(self, results) -> bool: + return results['loss'] < self.current_best_loss + def log_results(self, results) -> None: """ Log results if it is better than any previous evaluation """ print_all = self.config.get('print_all', False) - is_best_loss = results['loss'] < self.current_best_loss + is_best_loss = self.is_best(results) + + if not print_all: + print('.', end='' if results['current_epoch'] % 100 != 0 else None) # type: ignore + sys.stdout.flush() + if print_all or is_best_loss: if is_best_loss: self.current_best_loss = results['loss'] @@ -217,14 +237,10 @@ class Hyperopt: if print_all: print(log_str) else: - print('\n' + log_str) - else: - print('.', end='') - sys.stdout.flush() + print(f'\n{log_str}') def format_results_logstring(self, results) -> str: - # Output human-friendly index here (starting from 1) - current = results['current_epoch'] + 1 + current = results['current_epoch'] total = self.total_epochs res = results['results_explanation'] loss = results['loss'] @@ -336,7 +352,9 @@ class Hyperopt: return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' f'Total profit {total_profit: 11.8f} {stake_cur} ' - f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.') + f'({profit: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). ' + f'Avg duration {duration:5.1f} mins.' + ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions, cpu_count) -> Optimizer: return Optimizer( @@ -379,30 +397,19 @@ class Hyperopt: ) def start(self) -> None: - timerange = TimeRange.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) - data = load_data( - datadir=Path(self.config['datadir']), - pairs=self.config['exchange']['pair_whitelist'], - ticker_interval=self.backtesting.ticker_interval, - timerange=timerange - ) + data, timerange = self.backtesting.load_bt_data() - if not data: - logger.critical("No data found. Terminating.") - return + preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) + # Trim startup period from analyzed dataframe + for pair, df in preprocessed.items(): + preprocessed[pair] = trim_dataframe(df, timerange) min_date, max_date = get_timeframe(data) logger.info( 'Hyperopting with data from %s up to %s (%s days)..', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days + min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) - - preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) - dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt @@ -432,15 +439,19 @@ class Hyperopt: self.opt.tell(asked, [v['loss'] for v in f_val]) self.fix_optimizer_models_list() for j in range(jobs): - current = i * jobs + j + # Use human-friendly index here (starting from 1) + current = i * jobs + j + 1 val = f_val[j] val['current_epoch'] = current - val['is_initial_point'] = current < INITIAL_POINTS + val['is_initial_point'] = current <= INITIAL_POINTS + logger.debug(f"Optimizer epoch evaluated: {val}") + is_best = self.is_best(val) self.log_results(val) self.trials.append(val) - logger.debug(f"Optimizer epoch evaluated: {val}") + if is_best or current % 100 == 0: + self.save_trials() except KeyboardInterrupt: print('User interrupted..') - self.save_trials() + self.save_trials(final=True) self.log_trials_result() diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 4208b29d3..2b2a81f00 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -1,14 +1,13 @@ """ IHyperOpt interface -This module defines the interface to apply for hyperopts +This module defines the interface to apply for hyperopt """ import logging import math -from abc import ABC, abstractmethod +from abc import ABC from typing import Dict, Any, Callable, List -from pandas import DataFrame from skopt.space import Dimension, Integer, Real from freqtrade import OperationalException @@ -28,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 @@ -42,15 +41,6 @@ class IHyperOpt(ABC): # Assign ticker_interval to be used in hyperopt IHyperOpt.ticker_interval = str(config['ticker_interval']) - @staticmethod - @abstractmethod - def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Populate indicators that will be used in the Buy and Sell strategy. - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe(). - :return: A Dataframe with all mandatory indicators for the strategies. - """ - @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -116,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. @@ -127,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), diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py index b11b6e661..879a9f0e9 100644 --- a/freqtrade/optimize/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -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 diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 5afb0c4c2..d722e70f5 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -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 diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py new file mode 100644 index 000000000..d7b2c96ae --- /dev/null +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -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 diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py new file mode 100644 index 000000000..b3546ebd9 --- /dev/null +++ b/freqtrade/pairlist/PriceFilter.py @@ -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 diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 5896e814a..0050fbd5c 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -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']) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index b9b7977ab..2df9ba691 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -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)[:self._number_pairs] + 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: {self._whitelist}") + 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 diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py new file mode 100644 index 000000000..fa5382c37 --- /dev/null +++ b/freqtrade/pairlist/pairlistmanager.py @@ -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 diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 1850aafd9..735c740c3 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -8,17 +8,16 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, - create_engine, inspect) + create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Query from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker -from sqlalchemy import func from sqlalchemy.pool import StaticPool from freqtrade import OperationalException - logger = logging.getLogger(__name__) @@ -52,9 +51,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: raise OperationalException(f"Given value for db_url: '{db_url}' " f"is no valid database URL! (See {_SQL_DOCS_URL})") - session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) - Trade.session = session() - Trade.query = session.query_property() + # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope + # Scoped sessions proxy requests to the appropriate thread-local session. + # We should use the scoped_session object - not a seperately initialized version + Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) + Trade.query = Trade.session.query_property() _DECL_BASE.metadata.create_all(engine) check_migrate(engine) @@ -393,6 +394,37 @@ class Trade(_DECL_BASE): profit_percent = (close_trade_price / open_trade_price) - 1 return float(f"{profit_percent:.8f}") + @staticmethod + def get_trades(trade_filter=None) -> Query: + """ + Helper function to query Trades using filters. + :param trade_filter: Optional filter to apply to trades + Can be either a Filter object, or a List of filters + e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` + e.g. `(trade_filter=Trade.id == trade_id)` + :return: unsorted query object + """ + if trade_filter is not None: + if not isinstance(trade_filter, list): + trade_filter = [trade_filter] + return Trade.query.filter(*trade_filter) + else: + return Trade.query + + @staticmethod + def get_open_trades() -> List[Any]: + """ + Query trades from persistence layer + """ + return Trade.get_trades(Trade.is_open.is_(True)).all() + + @staticmethod + def get_open_order_trades(): + """ + Returns all open trades + """ + return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + @staticmethod def total_open_trades_stakes() -> float: """ @@ -405,11 +437,38 @@ class Trade(_DECL_BASE): return total_open_stake_amount or 0 @staticmethod - def get_open_trades() -> List[Any]: + def get_overall_performance() -> List[Dict[str, Any]]: """ - Query trades from persistence layer + Returns List of dicts containing all Trades, including profit and trade count """ - return Trade.query.filter(Trade.is_open.is_(True)).all() + pair_rates = Trade.session.query( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')) \ + .all() + return [ + { + 'pair': pair, + 'profit': rate, + 'count': count + } + for pair, rate, count in pair_rates + ] + + @staticmethod + def get_best_pair(): + """ + Get best pair with closed trade. + """ + best_pair = Trade.session.query( + Trade.pair, func.sum(Trade.close_profit).label('profit_sum') + ).filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')).first() + return best_pair @staticmethod def stoploss_reinitialization(desired_stoploss): diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6bd5993b6..57a02dd6b 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -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, @@ -264,12 +264,12 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], - trades: pd.DataFrame) -> go.Figure: + trades: pd.DataFrame, timeframe: str) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_tickers_with_mean(tickers, "close") # Add combined cumulative profit - df_comb = create_cum_profit(df_comb, trades, 'cum_profit') + df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) # Plot the pairs average close prices, and total profit growth avgclose = go.Scatter( @@ -293,19 +293,19 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], for pair in pairs: profit_col = f'cum_profit_{pair}' - df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) + df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, timeframe) fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") 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,15 +377,17 @@ 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"], trades) + fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], + trades, config.get('ticker_interval', '5m')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / "plot", auto_open=True) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index db51c3ca5..05efa1164 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -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, USERPATH_HYPEROPTS 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,12 +27,18 @@ 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')) + if not hasattr(self.hyperopt, 'populate_indicators'): + logger.warning("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.") if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Hyperopt class does not provide populate_buy_trend() method. " "Using populate_buy_trend from the strategy.") @@ -53,7 +58,7 @@ class HyperOptResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='hyperopts', extra_dir=extra_dir) + user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, object_name=hyperopt_name, kwargs={'config': config}) @@ -69,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, @@ -104,7 +110,7 @@ class HyperOptLossResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='hyperopts', extra_dir=extra_dir) + user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, object_name=hyper_loss_name) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 51c4f7dba..3bad42fd9 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -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 diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index c2782a219..d849f4ffb 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -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) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index d6fbe9a7a..9a76b9b74 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -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')) @@ -57,6 +60,7 @@ class StrategyResolver(IResolver): ("order_time_in_force", None, False), ("stake_currency", None, False), ("stake_amount", None, False), + ("startup_candle_count", None, False), ("use_sell_signal", True, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), @@ -125,7 +129,8 @@ class StrategyResolver(IResolver): current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir='strategies', extra_dir=extra_dir) + user_subdir=constants.USERPATH_STRATEGY, + extra_dir=extra_dir) if ":" in strategy_name: logger.info("loading base64 encoded strategy") diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 67bbfdc78..8f4cc4787 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -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) @@ -293,7 +312,7 @@ class ApiServer(RPC): logger.info("LocalRPC - Profit Command Called") stats = self._rpc_trade_statistics(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 @@ -332,7 +354,8 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ - results = self._rpc_balance(self._config.get('fiat_display_currency', '')) + results = self._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) return self.rest_dump(results) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f994ac006..4cebe646e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,17 +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 -import sqlalchemy as sql -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 @@ -82,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 @@ -118,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') @@ -131,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, @@ -154,12 +186,11 @@ class RPC: for day in range(0, timescale): profitday = today - timedelta(days=day) - trades = Trade.query \ - .filter(Trade.is_open.is_(False)) \ - .filter(Trade.close_date >= profitday)\ - .filter(Trade.close_date < (profitday + timedelta(days=1)))\ - .order_by(Trade.close_date)\ - .all() + trades = Trade.get_trades(trade_filter=[ + Trade.is_open.is_(False), + Trade.close_date >= profitday, + Trade.close_date < (profitday + timedelta(days=1)) + ]).order_by(Trade.close_date).all() curdayprofit = sum(trade.calc_profit() for trade in trades) profit_days[profitday] = { 'amount': f'{curdayprofit:.8f}', @@ -192,7 +223,7 @@ class RPC: def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: """ Returns cumulative profit statistics """ - trades = Trade.query.order_by(Trade.id).all() + trades = Trade.get_trades().order_by(Trade.id).all() profit_all_coin = [] profit_all_perc = [] @@ -221,15 +252,11 @@ 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) - best_pair = Trade.session.query( - Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum') - ).filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(sql.text('profit_sum DESC')).first() + best_pair = Trade.get_best_pair() if not best_pair: raise RPCException('no closed trade') @@ -270,34 +297,42 @@ class RPC: 'best_rate': round(bp_rate * 100, 2), } - def _rpc_balance(self, fiat_display_currency: str) -> Dict: + def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ output = [] total = 0.0 - for coin, balance in self._freqtrade.exchange.get_balances().items(): - if not balance['total']: + try: + tickers = self._freqtrade.exchange.get_tickers() + except (TemporaryError, DependencyException): + raise RPCException('Error getting current tickers.') + + for coin, balance in self._freqtrade.wallets.get_all_balances().items(): + if not balance.total: continue - if coin == 'BTC': + est_stake: float = 0 + if coin == stake_currency: rate = 1.0 + est_stake = balance.total else: try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC") - if pair.startswith("BTC"): - rate = 1.0 / self._freqtrade.get_sell_rate(pair, False) - else: - rate = self._freqtrade.get_sell_rate(pair, False) + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate = tickers.get(pair, {}).get('bid', None) + if rate: + if pair.startswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total except (TemporaryError, DependencyException): logger.warning(f" Could not get rate for pair {coin}.") continue - est_btc: float = rate * balance['total'] - total = total + est_btc + total = total + (est_stake or 0) output.append({ 'currency': coin, - 'free': balance['free'] if balance['free'] is not None else 0, - 'balance': balance['total'] if balance['total'] is not None else 0, - 'used': balance['used'] if balance['used'] is not None else 0, - 'est_btc': est_btc, + 'free': balance.free if balance.free is not None else 0, + 'balance': balance.total if balance.total is not None else 0, + 'used': balance.used if balance.used is not None else 0, + 'est_stake': est_stake or 0, + 'stake': stake_currency, }) if total == 0.0: if self._freqtrade.config.get('dry_run', False): @@ -389,11 +424,8 @@ class RPC: return {'result': 'Created sell orders for all open trades.'} # Query for trade - trade = Trade.query.filter( - sql.and_( - Trade.id == trade_id, - Trade.is_open.is_(True) - ) + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] ).first() if not trade: logger.warning('forcesell: Invalid argument received') @@ -423,7 +455,7 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() if trade: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -432,28 +464,20 @@ class RPC: # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): - trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() return trade else: return None - def _rpc_performance(self) -> List[Dict]: + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. Shows a performance statistic from finished trades """ - - pair_rates = Trade.session.query(Trade.pair, - sql.func.sum(Trade.close_profit).label('profit_sum'), - sql.func.count(Trade.pair).label('count')) \ - .filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(sql.text('profit_sum DESC')) \ - .all() - return [ - {'pair': pair, 'profit': round(rate * 100, 2), 'count': count} - for pair, rate, count in pair_rates - ] + pair_rates = Trade.get_overall_performance() + # Round and convert to % + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates] + return pair_rates def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ @@ -469,7 +493,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 } @@ -484,7 +508,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, } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 80582a0ce..2ae22f472 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -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"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @@ -323,15 +325,16 @@ class Telegram(RPC): def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ try: - result = self._rpc_balance(self._config.get('fiat_display_currency', '')) + result = self._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) output = '' for currency in result['currencies']: - if currency['est_btc'] > 0.0001: + if currency['est_stake'] > 0.0001: curr_output = "*{currency}:*\n" \ "\t`Available: {free: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ "\t`Pending: {used: .8f}`\n" \ - "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) + "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency) else: curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) @@ -549,6 +552,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 +573,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 diff --git a/freqtrade/state.py b/freqtrade/state.py index d4a2adba0..415f6f5f2 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -25,5 +25,12 @@ class RunMode(Enum): BACKTEST = "backtest" EDGE = "edge" HYPEROPT = "hyperopt" + UTIL_EXCHANGE = "util_exchange" + UTIL_NO_EXCHANGE = "util_no_exchange" PLOT = "plot" - OTHER = "other" # Used for plotting scripts and test + OTHER = "other" + + +TRADING_MODES = [RunMode.LIVE, RunMode.DRY_RUN] +OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT] +NON_UTIL_MODES = TRADING_MODES + OPTIMIZE_MODES diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index b839a9618..6c343b477 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -39,6 +39,9 @@ class DefaultStrategy(IStrategy): 'stoploss_on_exchange': False } + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + # Optional time in force for orders order_time_in_force = { 'buy': 'gtc', @@ -105,9 +108,6 @@ class DefaultStrategy(IStrategy): # EMA - Exponential Moving Average dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - return dataframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 014ca9968..e208138e7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -103,11 +103,14 @@ class IStrategy(ABC): # run "populate_indicators" only for new candle process_only_new_candles: bool = False + # Count of candles the strategy requires before producing valid signals + startup_candle_count: int = 0 + # 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 @@ -421,6 +424,7 @@ class IStrategy(ABC): def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """ Creates a dataframe and populates indicators for given ticker data + Used by optimize operations only, not during dry / live runs. """ return {pair: self.advise_indicators(pair_data, {'pair': pair}) for pair, pair_data in tickerdata.items()} diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 new file mode 100644 index 000000000..05ba08b81 --- /dev/null +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -0,0 +1,127 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +# --- Do not remove these libs --- +from functools import reduce +from typing import Any, Callable, Dict, List + +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +from freqtrade.optimize.hyperopt_interface import IHyperOpt + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class {{ hyperopt }}(IHyperOpt): + """ + This is a Hyperopt template to get you started. + + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md + + You should: + - Add any lib you need to build your hyperopt. + + You must keep: + - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. + + The roi_space, generate_roi_table, stoploss_space methods are no longer required to be + copied in every custom hyperopt. However, you may override them if you need the + 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. + Sample implementation of these methods can be found in + https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py + """ + + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + {{ buy_guards | indent(12) }} + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend + + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + {{ buy_space | indent(12) }} + ] + + @staticmethod + def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the sell strategy parameters to be used by Hyperopt. + """ + def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Sell strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + {{ sell_guards | indent(12) }} + + # TRIGGERS + if 'sell-trigger' in params: + if params['sell-trigger'] == 'sell-bb_upper': + conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['sell-trigger'] == 'sell-macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], dataframe['macd'] + )) + if params['sell-trigger'] == 'sell-sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['sar'], dataframe['close'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 + + return dataframe + + return populate_sell_trend + + @staticmethod + def sell_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching sell strategy parameters. + """ + return [ + {{ sell_space | indent(12) }} + ] diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 new file mode 100644 index 000000000..73a4c7a5a --- /dev/null +++ b/freqtrade/templates/base_strategy.py.j2 @@ -0,0 +1,138 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class {{ strategy }}(IStrategy): + """ + This is a strategy template to get you started. + More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + + You can: + :return: a Dataframe with all mandatory indicators for the strategies + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, + populate_sell_trend, hyperopt_space, buy_strategy_generator + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi". + minimal_roi = { + "60": 0.01, + "30": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Optimal ticker interval for the strategy. + ticker_interval = '5m' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional order type mapping. + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + {{ indicators | indent(8) }} + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + {{ buy_trend | indent(16) }} + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + {{ sell_trend | indent(16) }} + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'sell'] = 1 + return dataframe diff --git a/user_data/hyperopts/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py similarity index 86% rename from user_data/hyperopts/sample_hyperopt.py rename to freqtrade/templates/sample_hyperopt.py index fabfdb23e..f1dcb404a 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -1,19 +1,23 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List -from datetime import datetime -import numpy as np -import talib.abstract as ta +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib -class SampleHyperOpts(IHyperOpt): + +class SampleHyperOpt(IHyperOpt): """ This is a sample Hyperopt to inspire you. Feel free to customize it. @@ -34,34 +38,6 @@ class SampleHyperOpts(IHyperOpt): Sample implementation of these methods can be found in https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py """ - @staticmethod - def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Add several indicators needed for buy and sell strategies defined below. - """ - # ADX - dataframe['adx'] = ta.ADX(dataframe) - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - # Stochastic Fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - # Minus-DI - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_upperband'] = bollinger['upper'] - # SAR - dataframe['sar'] = ta.SAR(dataframe) - - return dataframe @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: diff --git a/user_data/hyperopts/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py similarity index 91% rename from user_data/hyperopts/sample_hyperopt_advanced.py rename to freqtrade/templates/sample_hyperopt_advanced.py index 00062a58d..5634c21ea 100644 --- a/user_data/hyperopts/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -1,20 +1,23 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# --- Do not remove these libs --- from functools import reduce -from math import exp from typing import Any, Callable, Dict, List -from datetime import datetime -import numpy as np# noqa F401 -import talib.abstract as ta +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.optimize.hyperopt_interface import IHyperOpt +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib -class AdvancedSampleHyperOpts(IHyperOpt): + +class AdvancedSampleHyperOpt(IHyperOpt): """ This is a sample hyperopt to inspire you. Feel free to customize it. @@ -37,6 +40,9 @@ class AdvancedSampleHyperOpts(IHyperOpt): """ @staticmethod def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class. + """ dataframe['adx'] = ta.ADX(dataframe) macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -229,8 +235,10 @@ class AdvancedSampleHyperOpts(IHyperOpt): def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators. Should be a copy of from strategy - must align to populate_indicators in this file + Based on TA indicators. + Can be a copy of the corresponding method from the strategy, + or will be loaded from the strategy. + Must align to populate_indicators used (either from this File, or from the strategy) Only used when --spaces does not include buy """ dataframe.loc[ @@ -246,8 +254,10 @@ class AdvancedSampleHyperOpts(IHyperOpt): def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators. Should be a copy of from strategy - must align to populate_indicators in this file + Based on TA indicators. + Can be a copy of the corresponding method from the strategy, + or will be loaded from the strategy. + Must align to populate_indicators used (either from this File, or from the strategy) Only used when --spaces does not include sell """ dataframe.loc[ diff --git a/user_data/hyperopts/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py similarity index 100% rename from user_data/hyperopts/sample_hyperopt_loss.py rename to freqtrade/templates/sample_hyperopt_loss.py diff --git a/user_data/strategies/sample_strategy.py b/freqtrade/templates/sample_strategy.py similarity index 61% rename from user_data/strategies/sample_strategy.py rename to freqtrade/templates/sample_strategy.py index 80c30283d..02bf24e7e 100644 --- a/user_data/strategies/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,13 +1,16 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # --- Do not remove these libs --- -from freqtrade.strategy.interface import IStrategy +import numpy as np # noqa +import pandas as pd # noqa from pandas import DataFrame -# -------------------------------- +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy # noqa # This class is a sample. Feel free to customize it. @@ -59,6 +62,9 @@ class SampleStrategy(IStrategy): sell_profit_only = False ignore_roi_if_buy_signal = False + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + # Optional order type mapping. order_types = { 'buy': 'limit', @@ -104,15 +110,20 @@ class SampleStrategy(IStrategy): # RSI dataframe['rsi'] = ta.RSI(dataframe) - """ # ADX dataframe['adx'] = ta.ADX(dataframe) - # Awesome oscillator - dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) - # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - dataframe['cci'] = ta.CCI(dataframe) + # # Awesome oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + # dataframe['cci'] = ta.CCI(dataframe) # MACD macd = ta.MACD(dataframe) @@ -123,40 +134,39 @@ class SampleStrategy(IStrategy): # MFI dataframe['mfi'] = ta.MFI(dataframe) - # Minus Directional Indicator / Movement - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Plus Directional Indicator / Movement - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # ROC - dataframe['roc'] = ta.ROC(dataframe) + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) - # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) - rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + # # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + # # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - # Stoch - stoch = ta.STOCH(dataframe) - dataframe['slowd'] = stoch['slowd'] - dataframe['slowk'] = stoch['slowk'] + # # Stoch + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] # Stoch fast stoch_fast = ta.STOCHF(dataframe) dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] - # Stoch RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] - """ + # # Stoch RSI + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] # Overlap Studies # ------------------------------------ @@ -167,21 +177,19 @@ class SampleStrategy(IStrategy): dataframe['bb_middleband'] = bollinger['mid'] dataframe['bb_upperband'] = bollinger['upper'] - """ - # EMA - Exponential Moving Average - dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) # SAR Parabol dataframe['sar'] = ta.SAR(dataframe) - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - """ - # TEMA - Triple Exponential Moving Average dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) @@ -194,65 +202,57 @@ class SampleStrategy(IStrategy): # Pattern Recognition - Bullish candlestick patterns # ------------------------------------ - """ - # Hammer: values [0, 100] - dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # Inverted Hammer: values [0, 100] - dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # Dragonfly Doji: values [0, 100] - dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # Piercing Line: values [0, 100] - dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # Morningstar: values [0, 100] - dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # Three White Soldiers: values [0, 100] - dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - """ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] # Pattern Recognition - Bearish candlestick patterns # ------------------------------------ - """ - # Hanging Man: values [0, 100] - dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # Shooting Star: values [0, 100] - dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # Gravestone Doji: values [0, 100] - dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # Dark Cloud Cover: values [0, 100] - dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # Evening Doji Star: values [0, 100] - dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # Evening Star: values [0, 100] - dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - """ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) # Pattern Recognition - Bullish/Bearish candlestick patterns # ------------------------------------ - """ - # Three Line Strike: values [0, -100, 100] - dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # Spinning Top: values [0, -100, 100] - dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # Engulfing: values [0, -100, 100] - dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # Harami: values [0, -100, 100] - dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # Three Outside Up/Down: values [0, -100, 100] - dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # Three Inside Up/Down: values [0, -100, 100] - dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - """ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - # Chart type - # ------------------------------------ - """ - # Heikinashi stategy - heikinashi = qtpylib.heikinashi(dataframe) - dataframe['ha_open'] = heikinashi['open'] - dataframe['ha_close'] = heikinashi['close'] - dataframe['ha_high'] = heikinashi['high'] - dataframe['ha_low'] = heikinashi['low'] - """ + # # Chart type + # # ------------------------------------ + # # Heikinashi stategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] # Retrieve best bid and best ask from the orderbook # ------------------------------------ diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb similarity index 90% rename from user_data/notebooks/strategy_analysis_example.ipynb rename to freqtrade/templates/strategy_analysis_example.ipynb index b9576e0bb..2876ea938 100644 --- a/user_data/notebooks/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -26,7 +26,7 @@ "# Customize these according to your needs.\n", "\n", "# Define some constants\n", - "ticker_interval = \"5m\"\n", + "timeframe = \"5m\"\n", "# Name of the strategy class\n", "strategy_name = 'SampleStrategy'\n", "# Path to user data\n", @@ -49,7 +49,7 @@ "from freqtrade.data.history import load_pair_history\n", "\n", "candles = load_pair_history(datadir=data_location,\n", - " ticker_interval=ticker_interval,\n", + " timeframe=timeframe,\n", " pair=pair)\n", "\n", "# Confirm success\n", @@ -68,9 +68,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "# Load strategy using values set above\n", @@ -169,6 +167,31 @@ "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the loaded trades for trade parallelism\n", + "This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n", + "\n", + "`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from freqtrade.data.btanalysis import analyze_trade_parallelism\n", + "\n", + "# Analyze the above\n", + "parallel_trades = analyze_trade_parallelism(trades, '5m')\n", + "\n", + "\n", + "parallel_trades.plot()" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/freqtrade/templates/subtemplates/buy_trend_full.j2 b/freqtrade/templates/subtemplates/buy_trend_full.j2 new file mode 100644 index 000000000..1a0d326b3 --- /dev/null +++ b/freqtrade/templates/subtemplates/buy_trend_full.j2 @@ -0,0 +1,3 @@ +(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle +(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising diff --git a/freqtrade/templates/subtemplates/buy_trend_minimal.j2 b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 new file mode 100644 index 000000000..6a4079cf3 --- /dev/null +++ b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 @@ -0,0 +1 @@ +(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 new file mode 100644 index 000000000..5b967f4ed --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 @@ -0,0 +1,8 @@ +if params.get('mfi-enabled'): + conditions.append(dataframe['mfi'] < params['mfi-value']) +if params.get('fastd-enabled'): + conditions.append(dataframe['fastd'] < params['fastd-value']) +if params.get('adx-enabled'): + conditions.append(dataframe['adx'] > params['adx-value']) +if params.get('rsi-enabled'): + conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 new file mode 100644 index 000000000..5e1022f59 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 @@ -0,0 +1,2 @@ +if params.get('rsi-enabled'): + conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 new file mode 100644 index 000000000..29bafbd93 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 @@ -0,0 +1,9 @@ +Integer(10, 25, name='mfi-value'), +Integer(15, 45, name='fastd-value'), +Integer(20, 50, name='adx-value'), +Integer(20, 40, name='rsi-value'), +Categorical([True, False], name='mfi-enabled'), +Categorical([True, False], name='fastd-enabled'), +Categorical([True, False], name='adx-enabled'), +Categorical([True, False], name='rsi-enabled'), +Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 new file mode 100644 index 000000000..5ddf537fb --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 @@ -0,0 +1,3 @@ +Integer(20, 40, name='rsi-value'), +Categorical([True, False], name='rsi-enabled'), +Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 new file mode 100644 index 000000000..bd7b499f4 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 @@ -0,0 +1,8 @@ +if params.get('sell-mfi-enabled'): + conditions.append(dataframe['mfi'] > params['sell-mfi-value']) +if params.get('sell-fastd-enabled'): + conditions.append(dataframe['fastd'] > params['sell-fastd-value']) +if params.get('sell-adx-enabled'): + conditions.append(dataframe['adx'] < params['sell-adx-value']) +if params.get('sell-rsi-enabled'): + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 new file mode 100644 index 000000000..8b4adebf6 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 @@ -0,0 +1,2 @@ +if params.get('sell-rsi-enabled'): + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 new file mode 100644 index 000000000..46469d532 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 @@ -0,0 +1,11 @@ +Integer(75, 100, name='sell-mfi-value'), +Integer(50, 100, name='sell-fastd-value'), +Integer(50, 100, name='sell-adx-value'), +Integer(60, 100, name='sell-rsi-value'), +Categorical([True, False], name='sell-mfi-enabled'), +Categorical([True, False], name='sell-fastd-enabled'), +Categorical([True, False], name='sell-adx-enabled'), +Categorical([True, False], name='sell-rsi-enabled'), +Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 new file mode 100644 index 000000000..dfb110543 --- /dev/null +++ b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 @@ -0,0 +1,5 @@ +Integer(60, 100, name='sell-rsi-value'), +Categorical([True, False], name='sell-rsi-enabled'), +Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 new file mode 100644 index 000000000..879a2daa0 --- /dev/null +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -0,0 +1,161 @@ + +# Momentum Indicators +# ------------------------------------ + +# RSI +dataframe['rsi'] = ta.RSI(dataframe) + +# ADX +dataframe['adx'] = ta.ADX(dataframe) + +# # Aroon, Aroon Oscillator +# aroon = ta.AROON(dataframe) +# dataframe['aroonup'] = aroon['aroonup'] +# dataframe['aroondown'] = aroon['aroondown'] +# dataframe['aroonosc'] = ta.AROONOSC(dataframe) + +# # Awesome oscillator +# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + +# # Commodity Channel Index: values Oversold:<-100, Overbought:>100 +# dataframe['cci'] = ta.CCI(dataframe) + +# MACD +macd = ta.MACD(dataframe) +dataframe['macd'] = macd['macd'] +dataframe['macdsignal'] = macd['macdsignal'] +dataframe['macdhist'] = macd['macdhist'] + +# MFI +dataframe['mfi'] = ta.MFI(dataframe) + +# # Minus Directional Indicator / Movement +# dataframe['minus_dm'] = ta.MINUS_DM(dataframe) +# dataframe['minus_di'] = ta.MINUS_DI(dataframe) + +# # Plus Directional Indicator / Movement +# dataframe['plus_dm'] = ta.PLUS_DM(dataframe) +# dataframe['plus_di'] = ta.PLUS_DI(dataframe) +# dataframe['minus_di'] = ta.MINUS_DI(dataframe) + +# # ROC +# dataframe['roc'] = ta.ROC(dataframe) + +# # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) +# rsi = 0.1 * (dataframe['rsi'] - 50) +# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + +# # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) +# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + +# # Stoch +# stoch = ta.STOCH(dataframe) +# dataframe['slowd'] = stoch['slowd'] +# dataframe['slowk'] = stoch['slowk'] + +# Stoch fast +stoch_fast = ta.STOCHF(dataframe) +dataframe['fastd'] = stoch_fast['fastd'] +dataframe['fastk'] = stoch_fast['fastk'] + +# # Stoch RSI +# stoch_rsi = ta.STOCHRSI(dataframe) +# dataframe['fastd_rsi'] = stoch_rsi['fastd'] +# dataframe['fastk_rsi'] = stoch_rsi['fastk'] + +# Overlap Studies +# ------------------------------------ + +# Bollinger bands +bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) +dataframe['bb_lowerband'] = bollinger['lower'] +dataframe['bb_middleband'] = bollinger['mid'] +dataframe['bb_upperband'] = bollinger['upper'] + +# # EMA - Exponential Moving Average +# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) +# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) +# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) +# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) +# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + +# # SMA - Simple Moving Average +# dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + +# SAR Parabol +dataframe['sar'] = ta.SAR(dataframe) + +# TEMA - Triple Exponential Moving Average +dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + +# Cycle Indicator +# ------------------------------------ +# Hilbert Transform Indicator - SineWave +hilbert = ta.HT_SINE(dataframe) +dataframe['htsine'] = hilbert['sine'] +dataframe['htleadsine'] = hilbert['leadsine'] + +# Pattern Recognition - Bullish candlestick patterns +# ------------------------------------ +# # Hammer: values [0, 100] +# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) +# # Inverted Hammer: values [0, 100] +# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) +# # Dragonfly Doji: values [0, 100] +# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) +# # Piercing Line: values [0, 100] +# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] +# # Morningstar: values [0, 100] +# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] +# # Three White Soldiers: values [0, 100] +# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + +# Pattern Recognition - Bearish candlestick patterns +# ------------------------------------ +# # Hanging Man: values [0, 100] +# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) +# # Shooting Star: values [0, 100] +# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) +# # Gravestone Doji: values [0, 100] +# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) +# # Dark Cloud Cover: values [0, 100] +# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) +# # Evening Doji Star: values [0, 100] +# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) +# # Evening Star: values [0, 100] +# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + +# Pattern Recognition - Bullish/Bearish candlestick patterns +# ------------------------------------ +# # Three Line Strike: values [0, -100, 100] +# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) +# # Spinning Top: values [0, -100, 100] +# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] +# # Engulfing: values [0, -100, 100] +# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] +# # Harami: values [0, -100, 100] +# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] +# # Three Outside Up/Down: values [0, -100, 100] +# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] +# # Three Inside Up/Down: values [0, -100, 100] +# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + +# # Chart type +# # ------------------------------------ +# # Heikinashi stategy +# heikinashi = qtpylib.heikinashi(dataframe) +# dataframe['ha_open'] = heikinashi['open'] +# dataframe['ha_close'] = heikinashi['close'] +# dataframe['ha_high'] = heikinashi['high'] +# dataframe['ha_low'] = heikinashi['low'] + +# Retrieve best bid and best ask from the orderbook +# ------------------------------------ +""" +# first check if dataprovider is available +if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] +""" diff --git a/freqtrade/templates/subtemplates/indicators_minimal.j2 b/freqtrade/templates/subtemplates/indicators_minimal.j2 new file mode 100644 index 000000000..7d75b4610 --- /dev/null +++ b/freqtrade/templates/subtemplates/indicators_minimal.j2 @@ -0,0 +1,17 @@ + +# Momentum Indicators +# ------------------------------------ + +# RSI +dataframe['rsi'] = ta.RSI(dataframe) + +# Retrieve best bid and best ask from the orderbook +# ------------------------------------ +""" +# first check if dataprovider is available +if self.dp: + if self.dp.runmode in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] +""" diff --git a/freqtrade/templates/subtemplates/sell_trend_full.j2 b/freqtrade/templates/subtemplates/sell_trend_full.j2 new file mode 100644 index 000000000..36c08c947 --- /dev/null +++ b/freqtrade/templates/subtemplates/sell_trend_full.j2 @@ -0,0 +1,3 @@ +(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle +(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling diff --git a/freqtrade/templates/subtemplates/sell_trend_minimal.j2 b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 new file mode 100644 index 000000000..42a7b81a2 --- /dev/null +++ b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 @@ -0,0 +1 @@ +(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 25e883c76..c71080d5a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,3 +1,4 @@ +import csv import logging import sys from collections import OrderedDict @@ -5,19 +6,21 @@ from pathlib import Path from typing import Any, Dict, List import arrow -import csv import rapidjson from tabulate import tabulate from freqtrade import OperationalException -from freqtrade.configuration import Configuration, TimeRange -from freqtrade.configuration.directory_operations import create_userdata_dir +from freqtrade.configuration import (Configuration, TimeRange, + remove_credentials) +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_userdata_dir) +from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) -from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, - symbol_is_pair) -from freqtrade.misc import plural +from freqtrade.exchange import (available_exchanges, ccxt_exchanges, + market_is_active, symbol_is_pair) +from freqtrade.misc import plural, render_template from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -33,14 +36,31 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str configuration = Configuration(args, method) config = configuration.get_config() - config['exchange']['dry_run'] = True # Ensure we do not use Exchange credentials - config['exchange']['key'] = '' - config['exchange']['secret'] = '' + remove_credentials(config) return config +def start_trading(args: Dict[str, Any]) -> int: + """ + Main entry point for trading mode + """ + from freqtrade.worker import Worker + # Load and run worker + worker = None + try: + worker = Worker(args) + worker.run() + except KeyboardInterrupt: + logger.info('SIGINT received, aborting ...') + finally: + if worker: + logger.info("worker found ... calling exit") + worker.exit() + return 0 + + def start_list_exchanges(args: Dict[str, Any]) -> None: """ Print available exchanges @@ -59,22 +79,105 @@ 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 """ if "user_data_dir" in args and args["user_data_dir"]: - create_userdata_dir(args["user_data_dir"], create_dir=True) + userdir = create_userdata_dir(args["user_data_dir"], create_dir=True) + copy_sample_files(userdir, overwrite=args["reset"]) else: logger.warning("`create-userdir` requires --userdir to be set.") sys.exit(1) +def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str): + """ + Deploy new strategy from template to strategy_path + """ + indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) + buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) + sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_strategy.py.j2', + arguments={"strategy": strategy_name, + "indicators": indicators, + "buy_trend": buy_trend, + "sell_trend": sell_trend, + }) + + logger.info(f"Writing strategy to `{strategy_path}`.") + strategy_path.write_text(strategy_text) + + +def start_new_strategy(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "strategy" in args and args["strategy"]: + if args["strategy"] == "DefaultStrategy": + raise OperationalException("DefaultStrategy is not allowed as name.") + + new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + + deploy_new_strategy(args['strategy'], new_path, args['template']) + + else: + raise OperationalException("`new-strategy` requires --strategy to be set.") + + +def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str): + """ + Deploys a new hyperopt template to hyperopt_path + """ + buy_guards = render_template( + templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",) + sell_guards = render_template( + templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",) + buy_space = render_template( + templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",) + sell_space = render_template( + templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_hyperopt.py.j2', + arguments={"hyperopt": hyperopt_name, + "buy_guards": buy_guards, + "sell_guards": sell_guards, + "buy_space": buy_space, + "sell_space": sell_space, + }) + + logger.info(f"Writing hyperopt to `{hyperopt_path}`.") + hyperopt_path.write_text(strategy_text) + + +def start_new_hyperopt(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "hyperopt" in args and args["hyperopt"]: + if args["hyperopt"] == "DefaultHyperopt": + raise OperationalException("DefaultHyperopt is not allowed as name.") + + new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args["hyperopt"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) + else: + raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") + + def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) """ - config = setup_utils_configuration(args, RunMode.OTHER) + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) timerange = TimeRange() if 'days' in config: @@ -123,7 +226,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: """ Print ticker intervals (timeframes) available on Exchange """ - config = setup_utils_configuration(args, RunMode.OTHER) + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Do not use ticker_interval set in the config config['ticker_interval'] = None @@ -144,7 +247,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: :param pairs_only: if True print only pairs, otherwise print all instruments (markets) :return: None """ - config = setup_utils_configuration(args, RunMode.OTHER) + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Init exchange exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 90b68c49d..c674b5286 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,7 +2,7 @@ """ Wallet """ import logging -from typing import Dict, NamedTuple +from typing import Dict, NamedTuple, Any from freqtrade.exchange import Exchange from freqtrade import constants @@ -72,3 +72,6 @@ class Wallets: ) logger.info('Wallets synced.') + + def get_all_balances(self) -> Dict[str, Any]: + return self._wallets diff --git a/mkdocs.yml b/mkdocs.yml index 2c3f70191..43d6acc1d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/requirements-common.txt b/requirements-common.txt index 1e42d8a04..e5c66590a 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,23 +1,24 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.1306 -SQLAlchemy==1.3.10 +ccxt==1.19.86 +SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 -arrow==0.15.2 +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 +jsonschema==3.2.0 TA-Lib==0.4.17 -tabulate==0.8.5 +tabulate==0.8.6 coinmarketcap==5.0.3 +jinja2==2.10.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 diff --git a/requirements-dev.txt b/requirements-dev.txt index f5cde59e8..a60bbf0eb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,15 +4,15 @@ -r requirements-hyperopt.txt coveralls==1.8.2 -flake8==3.7.8 +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.1 +pytest==5.3.0 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-mock==1.11.1 +pytest-mock==1.12.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==5.6.0 +nbconvert==5.6.1 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index f5dae7332..96a22b42e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.3.1 +scipy==1.3.3 scikit-learn==0.21.3 scikit-optimize==0.5.2 filelock==3.0.12 diff --git a/requirements-plot.txt b/requirements-plot.txt index 235c71896..87d5553b6 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.2.1 +plotly==4.3.0 diff --git a/requirements.txt b/requirements.txt index 8d9b4953f..ebf27abd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.17.3 -pandas==0.25.2 +numpy==1.17.4 +pandas==0.25.3 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index a46b3ebfb..ccb33604f 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -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") diff --git a/setup.py b/setup.py index 50b8eee9c..3710bcdc0 100644 --- a/setup.py +++ b/setup.py @@ -78,6 +78,7 @@ setup(name='freqtrade', 'python-rapidjson', 'sdnotify', 'colorama', + 'jinja2', # from requirements.txt 'numpy', 'pandas', diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 8af39d6ba..8f41b08fa 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -78,7 +78,7 @@ "ZEC/BTC", "XLM/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "ADA/BTC", "XMR/BTC" ], diff --git a/tests/conftest.py b/tests/conftest.py index 8e2b4e976..6c567dda8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,13 +55,16 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: +def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) + if mock_markets: + mocker.patch('freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=get_markets())) if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) @@ -69,8 +72,9 @@ def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange: - patch_exchange(mocker, api_mock, id) +def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', + mock_markets=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets) config["exchange"]["name"] = id try: exchange = ExchangeResolver(id, config).exchange @@ -85,6 +89,11 @@ def patch_wallet(mocker, free=999.9) -> None: )) +def patch_whitelist(mocker, conf) -> None: + mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_whitelist', + MagicMock(return_value=conf['exchange']['pair_whitelist'])) + + def patch_edge(mocker) -> None: # "ETH/BTC", # "LTC/BTC", @@ -120,6 +129,7 @@ def patch_freqtradebot(mocker, config) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) + patch_whitelist(mocker, config) def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: @@ -232,6 +242,9 @@ def default_conf(testdatadir): "HOT/BTC", ] }, + "pairlists": [ + {"method": "StaticPairList"} + ], "telegram": { "enabled": True, "token": "token", @@ -242,6 +255,7 @@ def default_conf(testdatadir): "db_url": "sqlite://", "user_data_dir": Path("user_data"), "verbosity": 3, + "strategy": "DefaultStrategy" } return configuration @@ -287,6 +301,10 @@ def ticker_sell_down(): @pytest.fixture def markets(): + return get_markets() + + +def get_markets(): return { 'ETH/BTC': { 'id': 'ethbtc', @@ -307,7 +325,7 @@ def markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -333,7 +351,7 @@ def markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -358,7 +376,7 @@ def markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -369,7 +387,7 @@ def markets(): 'symbol': 'LTC/BTC', 'base': 'LTC', 'quote': 'BTC', - 'active': False, + 'active': True, 'precision': { 'price': 8, 'amount': 8, @@ -383,7 +401,7 @@ def markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -394,7 +412,7 @@ def markets(): 'symbol': 'XRP/BTC', 'base': 'XRP', 'quote': 'BTC', - 'active': False, + 'active': True, 'precision': { 'price': 8, 'amount': 8, @@ -408,7 +426,7 @@ def markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -419,7 +437,7 @@ def markets(): 'symbol': 'NEO/BTC', 'base': 'NEO', 'quote': 'BTC', - 'active': False, + 'active': True, 'precision': { 'price': 8, 'amount': 8, @@ -433,7 +451,7 @@ def markets(): }, 'price': 500000, 'cost': { - 'min': 1, + 'min': 0.0001, 'max': 500000, }, }, @@ -444,7 +462,7 @@ def markets(): 'symbol': 'BTT/BTC', 'base': 'BTT', 'quote': 'BTC', - 'active': True, + 'active': False, 'precision': { 'base': 8, 'quote': 8, @@ -461,7 +479,7 @@ def markets(): 'max': None }, 'cost': { - 'min': 0.001, + 'min': 0.0001, 'max': None } }, @@ -494,7 +512,7 @@ def markets(): 'symbol': 'LTC/USDT', 'base': 'LTC', 'quote': 'USDT', - 'active': True, + 'active': False, 'precision': { 'amount': 8, 'price': 8 @@ -558,6 +576,72 @@ def 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=[]) @@ -852,6 +936,72 @@ 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': {} + }, + 'BTC/USDT': { + 'symbol': 'BTC/USDT', + 'timestamp': 1573758371399, + 'datetime': '2019-11-14T19:06:11.399Z', + 'high': 8800.0, + 'low': 8582.6, + 'bid': 8648.16, + 'bidVolume': 0.238771, + 'ask': 8648.72, + 'askVolume': 0.016253, + 'vwap': 8683.13647806, + 'open': 8759.7, + 'close': 8648.72, + 'last': 8648.72, + 'previousClose': 8759.67, + 'change': -110.98, + 'percentage': -1.267, + 'average': None, + 'baseVolume': 35025.943355, + 'quoteVolume': 304135046.4242901, + 'info': {} + }, 'ETH/USDT': { 'symbol': 'ETH/USDT', 'timestamp': 1522014804118, @@ -939,7 +1089,29 @@ def tickers(): 'baseVolume': 59698.79897, 'quoteVolume': 29132399.743954, 'info': {} - } + }, + 'XRP/BTC': { + 'symbol': 'XRP/BTC', + 'timestamp': 1573758257534, + 'datetime': '2019-11-14T19:04:17.534Z', + 'high': 3.126e-05, + 'low': 3.061e-05, + 'bid': 3.093e-05, + 'bidVolume': 27901.0, + 'ask': 3.095e-05, + 'askVolume': 10551.0, + 'vwap': 3.091e-05, + 'open': 3.119e-05, + 'close': 3.094e-05, + 'last': 3.094e-05, + 'previousClose': 3.117e-05, + 'change': -2.5e-07, + 'percentage': -0.802, + 'average': None, + 'baseVolume': 37334921.0, + 'quoteVolume': 1154.19266394, + 'info': {} + }, }) @@ -1189,8 +1361,8 @@ def rpc_balance(): 'used': 0.0 }, 'XRP': { - 'total': 1.0, - 'free': 1.0, + 'total': 0.1, + 'free': 0.01, 'used': 0.0 }, 'EUR': { diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 4068e00e4..13711c63e 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest from arrow import Arrow -from pandas import DataFrame, to_datetime +from pandas import DataFrame, DateOffset, to_datetime from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, @@ -10,7 +10,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades, - load_trades_from_db) + load_trades_from_db, analyze_trade_parallelism) from freqtrade.data.history import load_data, load_pair_history from tests.test_persistence import create_mock_trades @@ -32,7 +32,7 @@ def test_load_backtest_data(testdatadir): @pytest.mark.usefixtures("init_persistence") -def test_load_trades_db(default_conf, fee, mocker): +def test_load_trades_from_db(default_conf, fee, mocker): create_mock_trades(fee) # remove init so it does not init again @@ -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( @@ -84,6 +84,17 @@ def test_extract_trades_of_period(testdatadir): assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime +def test_analyze_trade_parallelism(default_conf, mocker, testdatadir): + filename = testdatadir / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + + res = analyze_trade_parallelism(bt_data, "5m") + assert isinstance(res, DataFrame) + assert 'open_trades' in res.columns + assert res['open_trades'].max() == 3 + assert res['open_trades'].min() == 0 + + def test_load_trades(default_conf, mocker): db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock()) bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock()) @@ -111,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) @@ -125,12 +136,30 @@ def test_create_cum_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = load_pair_history(pair="POWR/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'), - bt_data[bt_data["pair"] == 'POWR/BTC'], - "cum_profits") + bt_data[bt_data["pair"] == 'TRX/BTC'], + "cum_profits", timeframe="5m") + assert "cum_profits" in cum_profits.columns + assert cum_profits.iloc[0]['cum_profits'] == 0 + assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + + +def test_create_cum_profit1(testdatadir): + filename = testdatadir / "backtest-result_test.json" + bt_data = load_backtest_data(filename) + # Move close-time to "off" the candle, to make sure the logic still works + 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", timeframe='5m', + datadir=testdatadir, timerange=timerange) + + cum_profits = create_cum_profit(df.set_index('date'), + bt_data[bt_data["pair"] == 'TRX/BTC'], + "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index e773a970e..8184167b3 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -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 diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 9a857750b..1dbe20936 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -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): diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 95382768a..65feaf03e 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -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 ' @@ -95,6 +95,21 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N _clean_test_file(file) +def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> None: + 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', timeframe='1m', + datadir=testdatadir, timerange=timerange, + startup_candles=20, + ) + + assert ltfmock.call_count == 1 + assert ltfmock.call_args_list[0][1]['timerange'] != timerange + # startts is 20 minutes earlier + assert ltfmock.call_args_list[0][1]['timerange'].startts == timerange.startts - 20 * 60 + + def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, default_conf, testdatadir) -> None: """ @@ -107,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') @@ -254,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() @@ -271,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() @@ -292,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 @@ -311,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 ) @@ -337,8 +352,12 @@ def test_load_partial_missing(testdatadir, caplog) -> None: start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'], + startup_candles=20, timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + assert log_has( + 'Using indicator startup period: 20 ...', caplog + ) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(tickerdata['UNITTEST/BTC']) @@ -350,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)) @@ -371,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'] ) @@ -427,6 +446,46 @@ def test_trim_tickerlist(testdatadir) -> None: assert not ticker +def test_trim_dataframe(testdatadir) -> None: + data = history.load_data( + datadir=testdatadir, + timeframe='1m', + pairs=['UNITTEST/BTC'] + )['UNITTEST/BTC'] + min_date = int(data.iloc[0]['date'].timestamp()) + max_date = int(data.iloc[-1]['date'].timestamp()) + data_modify = data.copy() + + # Remove first 30 minutes (1800 s) + tr = TimeRange('date', None, min_date + 1800, 0) + data_modify = history.trim_dataframe(data_modify, tr) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 30 + assert all(data_modify.iloc[-1] == data.iloc[-1]) + assert all(data_modify.iloc[0] == data.iloc[30]) + + data_modify = data.copy() + # Remove last 30 minutes (1800 s) + tr = TimeRange(None, 'date', 0, max_date - 1800) + data_modify = history.trim_dataframe(data_modify, tr) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 30 + assert all(data_modify.iloc[0] == data.iloc[0]) + assert all(data_modify.iloc[-1] == data.iloc[-31]) + + data_modify = data.copy() + # Remove first 25 and last 30 minutes (1800 s) + tr = TimeRange('date', 'date', min_date + 1500, max_date - 1800) + data_modify = history.trim_dataframe(data_modify, tr) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 55 + # first row matches 25th original row + assert all(data_modify.iloc[0] == data.iloc[25]) + + def test_file_dump_json_tofile(testdatadir) -> None: file = testdatadir / 'test_{id}.json'.format(id=str(uuid.uuid4())) data = {'bar': 'foo'} @@ -458,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'] ) ) @@ -474,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 ) @@ -497,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 ) @@ -533,21 +592,22 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + + ex = get_patched_exchange(mocker, default_conf) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) ) - ex = get_patched_exchange(mocker, default_conf) timerange = TimeRange.parse_timerange("20190101-20190102") - unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], + unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], dl_path=testdatadir, timerange=timerange, erase=False ) assert dl_mock.call_count == 0 - assert "ETH/BTC" in unav_pairs - assert "XRP/BTC" in unav_pairs - assert log_has("Skipping pair ETH/BTC...", caplog) + assert "BTT/BTC" in unav_pairs + assert "LTC/USDT" in unav_pairs + assert log_has("Skipping pair BTT/BTC...", caplog) def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): @@ -609,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) @@ -626,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) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 5e244a97e..001dc9591 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -255,8 +255,8 @@ 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, - timerange=None, exchange=None): +def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, + timerange=None, exchange=None, *args, **kwargs): hz = 0.1 base = 0.001 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1e0a5fdc3..a21a5f3ac 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -14,13 +14,13 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, +from freqtrade.exchange.common import API_RETRY_COUNT +from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, + timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, - symbol_is_pair, - market_is_active) + timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re @@ -177,16 +177,11 @@ def test_symbol_amount_prec(default_conf, mocker): ''' Test rounds down to 4 Decimal places ''' - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' - }) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) - type(api_mock).markets = markets - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, id="binance") + mocker.patch('freqtrade.exchange.Exchange.markets', markets) amount = 2.34559 pair = 'ETH/BTC' @@ -198,16 +193,10 @@ def test_symbol_price_prec(default_conf, mocker): ''' Test rounds up to 4 decimal places ''' - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' - }) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}}) - type(api_mock).markets = markets - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, id="binance") + mocker.patch('freqtrade.exchange.Exchange.markets', markets) price = 2.34559 pair = 'ETH/BTC' @@ -279,7 +268,7 @@ def test__load_markets(default_conf, mocker, caplog): api_mock.load_markets = MagicMock(return_value=expected_return) type(api_mock).markets = expected_return default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] - ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) assert ex.markets == expected_return @@ -294,7 +283,8 @@ def test__reload_markets(default_conf, mocker, caplog): api_mock.load_markets = load_markets type(api_mock).markets = initial_markets default_conf['exchange']['markets_refresh_interval'] = 10 - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", + mock_markets=False) exchange._last_markets_refresh = arrow.utcnow().timestamp updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} @@ -533,6 +523,24 @@ def test_validate_order_types_not_in_config(default_conf, mocker): Exchange(conf) +def test_validate_required_startup_candles(default_conf, mocker, caplog): + api_mock = MagicMock() + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + + default_conf['startup_candle_count'] = 20 + ex = Exchange(default_conf) + assert ex + default_conf['startup_candle_count'] = 600 + + with pytest.raises(OperationalException, match=r'This strategy requires 600.*'): + Exchange(default_conf) + + def test_exchange_has(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) assert not exchange.exchange_has('ASDFASDF') @@ -1039,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 @@ -1099,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) @@ -1135,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*'): @@ -1578,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() @@ -1615,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', @@ -1715,15 +1725,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), # active markets ([], [], False, True, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', - 'TKN/BTC', 'XLTCUSDT']), + ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'NEO/BTC', + 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), # all pairs ([], [], True, False, ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), # active pairs ([], [], True, True, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC']), + ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'NEO/BTC', + 'TKN/BTC', 'XRP/BTC']), # all markets, base=ETH, LTC (['ETH', 'LTC'], [], False, False, ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index fdbaaa54d..8756143a0 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -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 diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 345e423cd..3f6cc8c9a 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -3,14 +3,13 @@ import logging from unittest.mock import MagicMock import pytest -from pandas import DataFrame from freqtrade.data.history import get_timeframe 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% @@ -294,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 @@ -313,7 +312,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions - data_processed = {pair: DataFrame()} + data_processed = {pair: frame.copy()} min_date, max_date = get_timeframe({pair: frame}) results = backtesting.backtest( { diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 998edda8a..e74ead33d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -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,8 +116,8 @@ 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, - timerange=None, exchange=None, live=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", fill_missing=True)} @@ -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) @@ -494,7 +494,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> def get_timeframe(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) - mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) + mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=None)) mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) @@ -511,10 +511,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> default_conf['timerange'] = '20180101-20180102' backtesting = Backtesting(default_conf) - backtesting.start() - # check the logs, that will contain the backtest result - - assert log_has('No data found. Terminating.', caplog) + with pytest.raises(OperationalException, match='No data found. Terminating.'): + backtesting.start() def test_backtest(default_conf, fee, mocker, testdatadir) -> None: @@ -524,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) @@ -578,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) @@ -690,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) @@ -716,9 +714,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) results = backtesting.backtest(backtest_conf) # Make sure we have parallel trades - assert len(evaluate_result_multi(results, '5min', 2)) > 0 + assert len(evaluate_result_multi(results, '5m', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades - assert len(evaluate_result_multi(results, '5min', 3)) == 0 + assert len(evaluate_result_multi(results, '5m', 3)) == 0 backtest_conf = { 'stake_amount': default_conf['stake_amount'], @@ -729,7 +727,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) 'end_date': max_date, } results = backtesting.backtest(backtest_conf) - assert len(evaluate_result_multi(results, '5min', 1)) == 0 + assert len(evaluate_result_multi(results, '5m', 1)) == 0 def test_backtest_record(default_conf, fee, mocker): @@ -819,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', @@ -838,6 +836,8 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', + 'Loading data from 2017-11-14T20:57:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' @@ -866,9 +866,10 @@ 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', + '--strategy-path', str(Path(__file__).parents[2] / 'freqtrade/templates'), '--ticker-interval', '1m', '--timerange', '1510694220-1510700340', '--enable-position-stacking', @@ -892,6 +893,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', + 'Loading data from 2017-11-14T20:57:00+00:00 ' + 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' 'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 2c45a8d51..ddfa7156e 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -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) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 052c3ba77..d3d544502 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 +import locale from datetime import datetime from pathlib import Path from unittest.mock import MagicMock, PropertyMock @@ -25,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': ['all']}) + default_conf.update({ + 'spaces': ['all'], + 'hyperopt': 'DefaultHyperOpt', + }) patch_exchange(mocker) return Hyperopt(default_conf) @@ -68,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) @@ -99,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', @@ -149,15 +155,20 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) hyperopt = DefaultHyperOpt + delattr(hyperopt, 'populate_indicators') delattr(hyperopt, 'populate_buy_trend') delattr(hyperopt, 'populate_sell_trend') mocker.patch( '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') + assert log_has("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.", caplog) assert log_has("Hyperopt class does not provide populate_sell_trend() method. " "Using populate_sell_trend from the strategy.", caplog) assert log_has("Hyperopt class does not provide populate_buy_trend() method. " @@ -169,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: @@ -179,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") @@ -187,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: @@ -198,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) @@ -215,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) @@ -228,7 +249,7 @@ def test_start(mocker, default_conf, caplog) -> None: def test_start_no_data(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) + mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=None)) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -237,14 +258,14 @@ 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) - start_hyperopt(args) - - assert log_has('No data found. Terminating.', caplog) + with pytest.raises(OperationalException, match='No data found. Terminating.'): + start_hyperopt(args) def test_start_filelock(mocker, default_conf, caplog) -> None: @@ -254,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) @@ -338,7 +360,7 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.log_results( { 'loss': 1, - 'current_epoch': 1, + 'current_epoch': 2, # This starts from 1 (in a human-friendly manner) 'results_explanation': 'foo.', 'is_initial_point': False } @@ -352,6 +374,7 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: hyperopt.log_results( { 'loss': 3, + 'current_epoch': 1, } ) assert caplog.record_tuples == [] @@ -360,13 +383,19 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None: trials = create_trials(mocker, hyperopt, testdatadir) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) - hyperopt.trials = trials - hyperopt.save_trials() - trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' - assert log_has(f"Saving 1 evaluations to '{trials_file}'", caplog) + + hyperopt.trials = trials + hyperopt.save_trials(final=True) + assert log_has("Saving 1 epoch.", caplog) + assert log_has(f"1 epoch saved to '{trials_file}'.", caplog) mock_dump.assert_called_once() + hyperopt.trials = trials + trials + hyperopt.save_trials(final=True) + assert log_has("Saving 2 epochs.", caplog) + assert log_has(f"2 epochs saved to '{trials_file}'.", caplog) + def test_read_trials_returns_trials_file(mocker, hyperopt, testdatadir, caplog) -> None: trials = create_trials(mocker, hyperopt, testdatadir) @@ -393,7 +422,8 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -407,6 +437,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': 'all', @@ -510,13 +541,15 @@ 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 = [ - ('POWR/BTC', 0.023117, 0.000233, 100) + ('TRX/BTC', 0.023117, 0.000233, 100) ] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] backtest_result = pd.DataFrame.from_records(trades, columns=labels) @@ -561,8 +594,9 @@ def test_generate_optimizer(mocker, default_conf) -> None: } response_expected = { 'loss': 1.9840569076926293, - 'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '( 2.31Σ%). Avg duration 100.0 mins.', + 'results_explanation': (' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' + '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 mins.' + ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params': optimizer_param, 'total_profit': 0.00023300 } @@ -576,6 +610,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': 'all', @@ -592,6 +627,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': 'all', @@ -608,7 +644,8 @@ def test_continue_hyperopt(mocker, default_conf, caplog): def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -621,6 +658,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', @@ -645,7 +683,8 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -658,6 +697,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', @@ -682,7 +722,8 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -696,6 +737,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', @@ -728,7 +770,8 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -737,6 +780,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', @@ -757,7 +801,8 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -770,6 +815,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', @@ -802,7 +848,8 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -815,6 +862,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', @@ -853,7 +901,8 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None ]) def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, method, space) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', + MagicMock(return_value=(MagicMock(), None))) mocker.patch( 'freqtrade.optimize.hyperopt.get_timeframe', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) @@ -862,6 +911,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, diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 411ae60a3..43285cdb1 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -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', 'BTT/BTC'] - freqtradebot.pairlists.refresh_pairlist() + whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/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', 'BTT/BTC']), - (False, "BTC", "bidVolume", ['BTT/BTC', 'TKN/BTC', 'ETH/BTC']), - (False, "USDT", "quoteVolume", ['ETH/USDT', 'LTC/USDT']), - (False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange - (True, "BTC", "quoteVolume", ["ETH/BTC", "TKN/BTC"]), - (True, "BTC", "bidVolume", ["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', 'XRP/BTC', 'HOT/BTC']), + # Different sorting depending on quote or bid volume + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], + "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/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', 'XRP/BTC']), + # Precisionfilter bid + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, + {"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/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', 'XRP/BTC']), + # Hot is removed by precision_filter, Fuel by low_price_filter. + ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.02} + ], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/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) + 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,70 @@ 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', 'LTC/BTC'], "Market is not active") # LTC/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): + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + whitelist_conf['pairlists'] = [] + + with pytest.raises(OperationalException, + match=r"No Pairlist defined!"): + get_patched_freqtradebot(mocker, whitelist_conf) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 66468927f..699f2d962 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -9,12 +9,11 @@ from numpy import isnan from freqtrade import DependencyException, TemporaryError from freqtrade.edge import PairInfo -from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import patch_exchange, patch_get_signal +from tests.conftest import patch_get_signal, get_patched_freqtradebot # Functions for recurrent object patching @@ -26,17 +25,15 @@ def prec_satoshi(a, b) -> float: # Unit tests -def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: +def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -98,43 +95,57 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: } == results[0] -def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +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', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) 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, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -143,7 +154,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -181,22 +192,20 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_exchange(mocker) 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', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -267,9 +276,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, +def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, ticker_sell_up, limit_buy_order, limit_sell_order): - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), @@ -281,10 +289,9 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -343,35 +350,23 @@ def test_rpc_balance_handle_error(default_conf, mocker): 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_exchange(mocker) 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', get_balances=MagicMock(return_value=mock_balance), - get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) + get_tickers=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() - - result = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 12) - assert prec_satoshi(result['value'], 180000) - assert 'USD' == result['symbol'] - assert result['currencies'] == [{ - 'currency': 'BTC', - 'free': 10.0, - 'balance': 12.0, - 'used': 2.0, - 'est_btc': 12.0, - }] - assert result['total'] == 12.0 + with pytest.raises(RPCException, match="Error getting current tickers."): + rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) -def test_rpc_balance_handle(default_conf, mocker): +def test_rpc_balance_handle(default_conf, mocker, tickers): mock_balance = { 'BTC': { 'free': 10.0, @@ -383,7 +378,7 @@ def test_rpc_balance_handle(default_conf, mocker): 'total': 5.0, 'used': 4.0, }, - 'PAX': { + 'USDT': { 'free': 5.0, 'total': 10.0, 'used': 5.0, @@ -394,58 +389,60 @@ def test_rpc_balance_handle(default_conf, mocker): 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_exchange(mocker) 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', get_balances=MagicMock(return_value=mock_balance), - get_ticker=MagicMock( - side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}), + get_tickers=tickers, get_valid_pair_combination=MagicMock( - side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}") + side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() - result = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 12.15) - assert prec_satoshi(result['value'], 182250) + result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12.309096315) + assert prec_satoshi(result['value'], 184636.44472997) assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', - 'free': 10.0, - 'balance': 12.0, - 'used': 2.0, - 'est_btc': 12.0, + 'free': 10.0, + 'balance': 12.0, + 'used': 2.0, + 'est_stake': 12.0, + 'stake': 'BTC', }, {'free': 1.0, 'balance': 5.0, 'currency': 'ETH', - 'est_btc': 0.05, - 'used': 4.0 + 'est_stake': 0.30794, + 'used': 4.0, + 'stake': 'BTC', + }, {'free': 5.0, 'balance': 10.0, - 'currency': 'PAX', - 'est_btc': 0.1, - 'used': 5.0} + 'currency': 'USDT', + 'est_stake': 0.0011563153318162476, + 'used': 5.0, + 'stake': 'BTC', + } ] - assert result['total'] == 12.15 + assert result['total'] == 12.309096315331816 def test_rpc_start(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED @@ -460,14 +457,13 @@ def test_rpc_start(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -483,14 +479,13 @@ def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stopbuy(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock() ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -501,8 +496,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: assert freqtradebot.config['max_open_trades'] == 0 -def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: - patch_exchange(mocker) +def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) cancel_order_mock = MagicMock() @@ -518,10 +512,9 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: } ), get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -606,18 +599,16 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: def test_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -641,18 +632,16 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert prec_satoshi(res[0]['profit'], 6.2) -def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: - patch_exchange(mocker) +def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) @@ -665,9 +654,8 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: assert counts["current"] == 1 -def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None: +def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None: default_conf['forcebuy_enable'] = True - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( @@ -675,11 +663,10 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order get_balances=MagicMock(return_value=ticker), get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), buy=buy_mm ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -703,8 +690,8 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order pair = 'XRP/BTC' # Test not buying - default_conf['stake_amount'] = 0.0000001 - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot.config['stake_amount'] = 0.0000001 patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'TKN/BTC' @@ -715,10 +702,9 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order def test_rpcforcebuy_stopped(mocker, default_conf) -> None: default_conf['forcebuy_enable'] = True default_conf['initial_state'] = 'stopped' - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -727,10 +713,9 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: def test_rpcforcebuy_disabled(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' @@ -739,69 +724,67 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: def test_rpc_whitelist(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + 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: - patch_exchange(mocker) - 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 = FreqtradeBot(default_conf) + 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'] def test_rpc_blacklist(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + 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'] def test_rpc_edge_disabled(mocker, default_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) with pytest.raises(RPCException, match=r'Edge is not enabled.'): rpc._rpc_edge() def test_rpc_edge_enabled(mocker, edge_conf) -> None: - patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - freqtradebot = FreqtradeBot(edge_conf) + freqtradebot = get_patched_freqtradebot(mocker, edge_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_edge() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b572a0514..555fcdc81 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,7 +23,7 @@ _TEST_PASS = "SuperSecurePassword1!" def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080", + "listen_port": 8080, "username": _TEST_USER, "password": _TEST_PASS, }}) @@ -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) @@ -129,7 +133,10 @@ def test_api__init__(default_conf, mocker): def test_api_run(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_port": 8080, + "username": "TestUser", + "password": "testPass", + }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) @@ -142,7 +149,7 @@ def test_api_run(default_conf, mocker, caplog): apiserver.run() assert server_mock.call_count == 1 assert server_mock.call_args_list[0][0][0] == "127.0.0.1" - assert server_mock.call_args_list[0][0][1] == "8080" + assert server_mock.call_args_list[0][0][1] == 8080 assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert hasattr(apiserver, "srv") @@ -154,14 +161,14 @@ def test_api_run(default_conf, mocker, caplog): server_mock.reset_mock() apiserver._config.update({"api_server": {"enabled": True, "listen_ip_address": "0.0.0.0", - "listen_port": "8089", + "listen_port": 8089, "password": "", }}) apiserver.run() assert server_mock.call_count == 1 assert server_mock.call_args_list[0][0][0] == "0.0.0.0" - assert server_mock.call_args_list[0][0][1] == "8089" + assert server_mock.call_args_list[0][0][1] == 8089 assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog) assert log_has("Starting Local Rest Server.", caplog) @@ -182,7 +189,10 @@ def test_api_run(default_conf, mocker, caplog): def test_api_cleanup(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"}}) + "listen_port": 8080, + "username": "TestUser", + "password": "testPass", + }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) @@ -220,28 +230,10 @@ def test_api_stopbuy(botclient): def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient - def mock_ticker(symbol, refresh): - if symbol == 'BTC/USDT': - return { - 'bid': 10000.00, - 'ask': 10000.00, - 'last': 10000.00, - } - elif symbol == 'XRP/BTC': - return { - 'bid': 0.00001, - 'ask': 0.00001, - 'last': 0.00001, - } - return { - 'bid': 0.1, - 'ask': 0.1, - 'last': 0.1, - } mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") + ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) @@ -252,7 +244,8 @@ def test_api_balance(botclient, mocker, rpc_balance): 'free': 12.0, 'balance': 12.0, 'used': 0.0, - 'est_btc': 12.0, + 'est_stake': 12.0, + 'stake': 'BTC', } @@ -280,6 +273,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 +418,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 +461,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 +469,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 +479,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): diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 7278f0671..edf6bae4d 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 - +import time import logging from unittest.mock import MagicMock @@ -173,9 +173,14 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None: default_conf["telegram"]["enabled"] = False default_conf["api_server"] = {"enabled": True, "listen_ip_address": "127.0.0.1", - "listen_port": "8080"} + "listen_port": 8080, + "username": "TestUser", + "password": "TestPass", + } 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] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a776ad5df..367eb8366 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -22,7 +22,7 @@ from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal) + patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -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) @@ -143,17 +143,15 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: assert log_has('Exception occurred within Telegram module', caplog) -def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: - update.message.chat.id = 123 +def test_status(default_conf, update, mocker, fee, ticker,) -> None: + update.message.chat.id = "123" default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = 123 + default_conf['telegram']['chat_id'] = "123" - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() status_table = MagicMock() @@ -184,9 +182,8 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: _status_table=status_table, _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -204,13 +201,11 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: assert status_table.call_count == 1 -def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() status_table = MagicMock() @@ -220,9 +215,9 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No _status_table=status_table, _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -256,14 +251,12 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] -def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() mocker.patch.multiple( @@ -271,10 +264,9 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) default_conf['stake_amount'] = 15.0 - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -307,8 +299,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, - limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_sell_order, mocker) -> None: default_conf['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -318,7 +309,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() mocker.patch.multiple( @@ -326,9 +316,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -382,7 +371,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker @@ -393,9 +381,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -420,14 +407,12 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets) ) msg_mock = MagicMock() mocker.patch.multiple( @@ -435,9 +420,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -477,29 +461,10 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> None: - - def mock_ticker(symbol, refresh): - if symbol == 'BTC/USDT': - return { - 'bid': 10000.00, - 'ask': 10000.00, - 'last': 10000.00, - } - elif symbol == 'XRP/BTC': - return { - 'bid': 0.00001, - 'ask': 0.00001, - 'last': 0.00001, - } - return { - 'bid': 0.1, - 'ask': 0.1, - 'last': 0.1, - } +def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") @@ -580,7 +545,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'free': 1.0, 'used': 0.5, 'balance': i, - 'est_btc': 1 + 'est_stake': 1, + 'stake': 'BTC', }) mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ 'currencies': balances, @@ -724,16 +690,16 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: def test_forcesell_handle(default_conf, update, ticker, fee, - ticker_sell_up, markets, mocker) -> None: + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) + patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), ) freqtradebot = FreqtradeBot(default_conf) @@ -775,17 +741,18 @@ def test_forcesell_handle(default_conf, update, ticker, fee, def test_forcesell_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, markets, mocker) -> None: + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) + patch_whitelist(mocker, default_conf) + mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), ) freqtradebot = FreqtradeBot(default_conf) @@ -830,17 +797,17 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, } == last_msg -def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: +def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) @@ -885,9 +852,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - patch_exchange(mocker) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -980,8 +946,7 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non def test_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_exchange(mocker) + limit_buy_order, limit_sell_order, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -992,10 +957,8 @@ def test_performance_handle(default_conf, update, ticker, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(markets), ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -1018,8 +981,7 @@ def test_performance_handle(default_conf, update, ticker, fee, assert 'ETH/BTC\t6.20% (1)' in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_exchange(mocker) +def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1030,10 +992,9 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), - markets=PropertyMock(markets) + get_fee=fee, ) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) @@ -1071,8 +1032,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: @@ -1083,17 +1044,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: @@ -1195,6 +1156,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( diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index dbbc4cefb..c066aa8e7 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -113,7 +113,7 @@ def test_send_msg(default_conf, mocker): def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() - default_conf["webhook"]["webhookbuy"] = None + del default_conf["webhook"]["webhookbuy"] webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 12770f2c7..963d36c76 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -36,13 +36,15 @@ def test_search_strategy(): def test_load_strategy(default_conf, result): - default_conf.update({'strategy': 'SampleStrategy'}) + default_conf.update({'strategy': 'SampleStrategy', + 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') + }) resolver = StrategyResolver(default_conf) assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_base64(result, caplog, default_conf): - with open("user_data/strategies/sample_strategy.py", "rb") as file: + with (Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py').open("rb") as file: encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) @@ -54,21 +56,30 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): + default_conf['strategy'] = 'DefaultStrategy' resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' - resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir) + resolver._load_strategy('DefaultStrategy', 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): diff --git a/tests/test_arguments.py b/tests/test_arguments.py index e874e6769..d8fbace0f 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -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: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 5d05a1e68..ae85c7493 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -11,15 +11,13 @@ import pytest from jsonschema import Draft4Validator, ValidationError, validate from freqtrade import OperationalException, constants -from freqtrade.configuration import (Arguments, Configuration, +from freqtrade.configuration import (Arguments, Configuration, check_exchange, + remove_credentials, validate_config_consistency) -from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import ( check_conflicting_settings, process_deprecated_setting, process_temporary_deprecated_settings) -from freqtrade.configuration.directory_operations import (create_datadir, - create_userdata_dir) from freqtrade.configuration.load_config import load_config_file from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.loggers import _set_loggers, setup_logging @@ -43,10 +41,16 @@ def test_load_config_invalid_pair(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None: - default_conf.pop('exchange') + conf = deepcopy(default_conf) + conf.pop('exchange') with pytest.raises(ValidationError, match=r".*'exchange' is a required property.*"): - validate_config_schema(default_conf) + validate_config_schema(conf) + + conf = deepcopy(default_conf) + conf.pop('stake_currency') + with pytest.raises(ValidationError, match=r".*'stake_currency' is a required property.*"): + validate_config_schema(conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: @@ -69,7 +73,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 = {} @@ -97,13 +101,12 @@ 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() assert validated_conf['max_open_trades'] == 0 assert 'internals' in validated_conf - assert log_has('Validating configuration ...', caplog) def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: @@ -122,7 +125,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() @@ -135,7 +138,6 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist'] assert 'internals' in validated_conf - assert log_has('Validating configuration ...', caplog) def test_from_config(default_conf, mocker, caplog) -> None: @@ -162,7 +164,6 @@ def test_from_config(default_conf, mocker, caplog) -> None: assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist'] assert validated_conf['fiat_display_currency'] == "EUR" assert 'internals' in validated_conf - assert log_has('Validating configuration ...', caplog) assert isinstance(validated_conf['user_data_dir'], Path) @@ -188,13 +189,12 @@ 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() assert validated_conf['max_open_trades'] > 999999999 assert validated_conf['max_open_trades'] == float('inf') - assert log_has('Validating configuration ...', caplog) assert "runmode" in validated_conf assert validated_conf['runmode'] == RunMode.DRY_RUN @@ -212,11 +212,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 @@ -225,6 +224,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', @@ -244,6 +244,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' ] @@ -260,6 +261,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' ] @@ -276,6 +278,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' ] @@ -294,6 +297,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' ] @@ -304,6 +308,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', @@ -311,7 +332,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() @@ -323,6 +344,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', ] @@ -339,9 +361,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() @@ -377,11 +399,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', @@ -428,8 +450,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', @@ -546,18 +568,30 @@ def test_check_exchange(default_conf, caplog) -> None: # Test no exchange... default_conf.get('exchange').update({'name': ''}) - default_conf['runmode'] = RunMode.OTHER + default_conf['runmode'] = RunMode.UTIL_EXCHANGE with pytest.raises(OperationalException, match=r'This command requires a configured exchange.*'): check_exchange(default_conf) +def test_remove_credentials(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['dry_run'] = False + remove_credentials(conf) + + assert conf['dry_run'] is True + assert conf['exchange']['key'] == '' + assert conf['exchange']['secret'] == '' + assert conf['exchange']['password'] == '' + assert conf['exchange']['uid'] == '' + + def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) # Prevent setting loggers mocker.patch('freqtrade.loggers._set_loggers', MagicMock) - arglist = ['-vvv'] + arglist = ['trade', '-vvv'] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) @@ -659,7 +693,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) @@ -675,7 +709,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() @@ -687,45 +721,6 @@ def test_validate_default_conf(default_conf) -> None: validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) -def test_create_datadir(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - create_datadir(default_conf, '/foo/bar') - assert md.call_args[1]['parents'] is True - assert log_has('Created data directory: /foo/bar', caplog) - - -def test_create_userdata_dir(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - x = create_userdata_dir('/tmp/bar', create_dir=True) - assert md.call_count == 7 - assert md.call_args[1]['parents'] is False - assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) - assert isinstance(x, Path) - assert str(x) == str(Path("/tmp/bar")) - - -def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - create_userdata_dir('/tmp/bar') - assert md.call_count == 0 - - -def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: - mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) - md = mocker.patch.object(Path, 'mkdir', MagicMock()) - - with pytest.raises(OperationalException, - match=r'Directory `.{1,2}tmp.{1,2}bar` does not exist.*'): - create_userdata_dir('/tmp/bar', create_dir=False) - assert md.call_count == 0 - - def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' @@ -780,6 +775,30 @@ def test_validate_edge(edge_conf): validate_config_consistency(edge_conf) +def test_validate_whitelist(default_conf): + default_conf['runmode'] = RunMode.DRY_RUN + # Test regular case - has whitelist and uses StaticPairlist + validate_config_consistency(default_conf) + conf = deepcopy(default_conf) + del conf['exchange']['pair_whitelist'] + # Test error case + with pytest.raises(OperationalException, + match="StaticPairList requires pair_whitelist to be set."): + + validate_config_consistency(conf) + + conf = deepcopy(default_conf) + + conf.update({"pairlists": [{ + "method": "VolumePairList", + }]}) + # Dynamic whitelist should not care about pair_whitelist + validate_config_consistency(conf) + del conf['exchange']['pair_whitelist'] + + validate_config_consistency(conf) + + def test_load_config_test_comments() -> None: """ Load config with comments @@ -852,7 +871,7 @@ def test_pairlist_resolving(): args = Arguments(arglist).get_parsed_arg() - configuration = Configuration(args) + configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] @@ -862,8 +881,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() @@ -876,8 +895,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', ] @@ -898,8 +917,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', ] @@ -920,8 +939,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', ] @@ -946,7 +965,7 @@ def test_pairlist_resolving_fallback(mocker): # Fix flaky tests if config.json exists args["config"] = None - configuration = Configuration(args) + configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] @@ -990,6 +1009,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) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py new file mode 100644 index 000000000..db41e2da2 --- /dev/null +++ b/tests/test_directory_operations.py @@ -0,0 +1,91 @@ +# pragma pylint: disable=missing-docstring, protected-access, invalid-name +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from freqtrade import OperationalException +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_datadir, + create_userdata_dir) +from tests.conftest import log_has, log_has_re + + +def test_create_datadir(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + create_datadir(default_conf, '/foo/bar') + assert md.call_args[1]['parents'] is True + assert log_has('Created data directory: /foo/bar', caplog) + + +def test_create_userdata_dir(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + x = create_userdata_dir('/tmp/bar', create_dir=True) + assert md.call_count == 8 + assert md.call_args[1]['parents'] is False + assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) + assert isinstance(x, Path) + assert str(x) == str(Path("/tmp/bar")) + + +def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + create_userdata_dir('/tmp/bar') + assert md.call_count == 0 + + +def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + md = mocker.patch.object(Path, 'mkdir', MagicMock()) + + with pytest.raises(OperationalException, + match=r'Directory `.{1,2}tmp.{1,2}bar` does not exist.*'): + create_userdata_dir('/tmp/bar', create_dir=False) + assert md.call_count == 0 + + +def test_copy_sample_files(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + copymock = mocker.patch('shutil.copy', MagicMock()) + + copy_sample_files(Path('/tmp/bar')) + assert copymock.call_count == 5 + assert copymock.call_args_list[0][0][1] == str( + Path('/tmp/bar') / 'strategies/sample_strategy.py') + assert copymock.call_args_list[1][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt_advanced.py') + assert copymock.call_args_list[2][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt_loss.py') + assert copymock.call_args_list[3][0][1] == str( + Path('/tmp/bar') / 'hyperopts/sample_hyperopt.py') + assert copymock.call_args_list[4][0][1] == str( + Path('/tmp/bar') / 'notebooks/strategy_analysis_example.ipynb') + + +def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: + mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + mocker.patch('shutil.copy', MagicMock()) + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + + mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False])) + + with pytest.raises(OperationalException, + match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\."): + copy_sample_files(Path('/tmp/bar')) + mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + copy_sample_files(Path('/tmp/bar')) + assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog) + caplog.clear() + copy_sample_files(Path('/tmp/bar'), overwrite=True) + assert log_has_re(r"File `.*` exists already, overwriting\.", caplog) diff --git a/tests/test_docs.sh b/tests/test_docs.sh new file mode 100755 index 000000000..09e142b99 --- /dev/null +++ b/tests/test_docs.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Test Documentation boxes - +# !!! : is not allowed! +# !!! "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 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8aefaba17..fe58ccf37 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -14,7 +14,6 @@ import requests from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError, constants) from freqtrade.constants import MATH_CLOSE_PREC -from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -23,7 +22,7 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.worker import Worker from tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_get_signal, patch_wallet) + patch_get_signal, patch_wallet, patch_whitelist) def patch_RPCManager(mocker) -> MagicMock: @@ -49,16 +48,6 @@ def test_freqtradebot_state(mocker, default_conf, markets) -> None: assert freqtrade.state is State.STOPPED -def test_worker_state(mocker, default_conf, markets) -> None: - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - worker = get_patched_worker(mocker, default_conf) - assert worker.state is State.RUNNING - - default_conf.pop('initial_state') - worker = Worker(args=None, config=default_conf) - assert worker.state is State.STOPPED - - def test_cleanup(mocker, default_conf, caplog) -> None: mock_cleanup = MagicMock() mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) @@ -68,69 +57,6 @@ def test_cleanup(mocker, default_conf, caplog) -> None: assert mock_cleanup.call_count == 1 -def test_worker_running(mocker, default_conf, caplog) -> None: - mock_throttle = MagicMock() - mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) - mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock()) - - worker = get_patched_worker(mocker, default_conf) - - state = worker._worker(old_state=None) - assert state is State.RUNNING - assert log_has('Changing state to: RUNNING', caplog) - assert mock_throttle.call_count == 1 - # Check strategy is loaded, and received a dataprovider object - assert worker.freqtrade.strategy - assert worker.freqtrade.strategy.dp - assert isinstance(worker.freqtrade.strategy.dp, DataProvider) - - -def test_worker_stopped(mocker, default_conf, caplog) -> None: - mock_throttle = MagicMock() - mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) - mock_sleep = mocker.patch('time.sleep', return_value=None) - - worker = get_patched_worker(mocker, default_conf) - worker.state = State.STOPPED - state = worker._worker(old_state=State.RUNNING) - assert state is State.STOPPED - assert log_has('Changing state to: STOPPED', caplog) - assert mock_throttle.call_count == 0 - assert mock_sleep.call_count == 1 - - -def test_throttle(mocker, default_conf, caplog) -> None: - def throttled_func(): - return 42 - - caplog.set_level(logging.DEBUG) - worker = get_patched_worker(mocker, default_conf) - - start = time.time() - result = worker._throttle(throttled_func, min_secs=0.1) - end = time.time() - - assert result == 42 - assert end - start > 0.1 - assert log_has('Throttling throttled_func for 0.10 seconds', caplog) - - result = worker._throttle(throttled_func, min_secs=-1) - assert result == 42 - - -def test_throttle_with_assets(mocker, default_conf) -> None: - def throttled_func(nb_assets=-1): - return nb_assets - - worker = get_patched_worker(mocker, default_conf) - - result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666) - assert result == 666 - - result = worker._throttle(throttled_func, min_secs=0.1) - assert result == -1 - - def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -224,18 +150,13 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade._get_trade_stake_amount('ETH/BTC') -def test_get_trade_stake_amount_unlimited_amount(default_conf, - ticker, - limit_buy_order, - fee, - markets, - mocker) -> None: +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, + limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount']) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee @@ -296,7 +217,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, edge_conf) -> None: +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -317,7 +238,6 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) ############################################# @@ -337,7 +257,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, assert trade.sell_reason == SellType.STOP_LOSS.value -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, +def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -358,7 +278,6 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) ############################################# @@ -377,17 +296,16 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, def test_total_open_trades_stakes(mocker, default_conf, ticker, - limit_buy_order, fee, markets) -> None: + limit_buy_order, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - default_conf['stake_amount'] = 0.0000098751 + default_conf['stake_amount'] = 0.00098751 default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -395,7 +313,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, trade = Trade.query.first() assert trade is not None - assert trade.stake_amount == 0.0000098751 + assert trade.stake_amount == 0.00098751 assert trade.is_open assert trade.open_date is not None @@ -403,11 +321,11 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None - assert trade.stake_amount == 0.0000098751 + assert trade.stake_amount == 0.00098751 assert trade.is_open assert trade.open_date is not None - assert Trade.total_open_trades_stakes() == 1.97502e-05 + assert Trade.total_open_trades_stakes() == 1.97502e-03 def test_get_min_pair_stake_amount(mocker, default_conf) -> None: @@ -416,6 +334,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.stoploss = -0.05 markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + # no pair found mocker.patch( 'freqtrade.exchange.Exchange.markets', @@ -507,7 +426,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == min(2, 2 * 2) / 0.9 + assert result == max(2, 2 * 2) / 0.9 # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -519,10 +438,30 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == min(8, 2 * 2) / 0.9 + assert result == max(8, 2 * 2) / 0.9 -def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: +def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + + # Real Binance data + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 0.0001}, + 'amount': {'min': 0.001} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 0.020405) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + + +def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -530,7 +469,6 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mock get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) # Save state of current whitelist @@ -556,7 +494,7 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mock def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) @@ -565,7 +503,6 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -575,7 +512,7 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -584,7 +521,6 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, get_ticker=ticker, buy=buy_mock, get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['stake_amount'] = 0.0005 freqtrade = FreqtradeBot(default_conf) @@ -596,7 +532,7 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -605,11 +541,11 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or get_ticker=ticker, buy=buy_mock, get_fee=fee, - markets=PropertyMock(return_value=markets) ) - default_conf['stake_amount'] = 0.000000005 freqtrade = FreqtradeBot(default_conf) + freqtrade.config['stake_amount'] = 0.000000005 + patch_get_signal(freqtrade) assert not freqtrade.create_trades() @@ -625,7 +561,6 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['max_open_trades'] = 0 default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT @@ -638,7 +573,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -646,7 +581,6 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] @@ -660,7 +594,7 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - markets, mocker, caplog) -> None: + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -668,7 +602,6 @@ def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_ord get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['exchange']['pair_whitelist'] = [] freqtrade = FreqtradeBot(default_conf) @@ -699,7 +632,7 @@ def test_create_trades_no_signal(default_conf, fee, mocker) -> None: @pytest.mark.parametrize("max_open", range(0, 5)) def test_create_trades_multiple_trades(default_conf, ticker, - fee, markets, mocker, max_open) -> None: + fee, mocker, max_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = max_open @@ -708,7 +641,6 @@ def test_create_trades_multiple_trades(default_conf, ticker, get_ticker=ticker, buy=MagicMock(return_value={'id': "12355555"}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -719,7 +651,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, assert len(trades) == max_open -def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> None: +def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = 4 @@ -728,7 +660,6 @@ def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> No get_ticker=ticker, buy=MagicMock(return_value={'id': "12355555"}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -747,13 +678,12 @@ def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> No def test_process_trade_creation(default_conf, ticker, limit_buy_order, - markets, fee, mocker, caplog) -> None: + fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -782,13 +712,12 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, ) -def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None: +def test_process_exchange_failures(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=TemporaryError) ) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) @@ -800,13 +729,12 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non assert sleep_mock.has_calls() -def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None: +def test_process_operational_exception(default_conf, ticker, mocker) -> None: msg_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=OperationalException) ) worker = Worker(args=None, config=default_conf) @@ -819,14 +747,12 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] -def test_process_trade_handling( - default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: +def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -846,15 +772,14 @@ def test_process_trade_handling( assert len(trades) == 1 -def test_process_trade_no_whitelist_pair( - default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: +def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, + fee, mocker) -> None: """ Test process with trade not in pair list """ patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -891,7 +816,7 @@ def test_process_trade_no_whitelist_pair( assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist)) -def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None: +def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -902,7 +827,6 @@ def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - markets=PropertyMock(return_value=markets), buy=MagicMock(side_effect=TemporaryError), refresh_latest_ohlcv=refresh_mock, ) @@ -948,7 +872,7 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None: assert freqtrade.get_target_bid('ETH/BTC') == 5 -def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None: +def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) @@ -970,7 +894,6 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non }), buy=buy_mm, get_fee=fee, - markets=PropertyMock(return_value=markets) ) pair = 'ETH/BTC' @@ -1067,7 +990,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) patch_exchange(mocker) @@ -1081,7 +1004,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), stoploss_limit=stoploss_limit ) freqtrade = FreqtradeBot(default_conf) @@ -1168,7 +1090,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # Sixth case: stoploss order was cancelled but couldn't create new one patch_RPCManager(mocker) patch_exchange(mocker) @@ -1182,7 +1104,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), get_order=MagicMock(return_value={'status': 'canceled'}), stoploss_limit=MagicMock(side_effect=DependencyException()), ) @@ -1203,7 +1124,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, - markets, limit_buy_order, limit_sell_order): + limit_buy_order, limit_sell_order): rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) @@ -1217,7 +1138,6 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=sell_mock, get_fee=fee, - markets=PropertyMock(return_value=markets), get_order=MagicMock(return_value={'status': 'canceled'}), stoploss_limit=MagicMock(side_effect=InvalidOrderException()), ) @@ -1247,11 +1167,10 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ @@ -1262,7 +1181,6 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), stoploss_limit=stoploss_limit ) @@ -1272,7 +1190,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, # disabling ROI default_conf['minimal_roi']['0'] = 999999999 - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) # enabling stoploss on exchange freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1342,8 +1260,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, - markets, limit_buy_order, - limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss_limit = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1358,7 +1275,6 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), stoploss_limit=stoploss_limit ) @@ -1411,7 +1327,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, - markets, limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss_limit = MagicMock(return_value={'id': 13434334}) @@ -1429,7 +1345,6 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), stoploss_limit=stoploss_limit ) @@ -1730,8 +1645,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde assert not trade.is_open -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, - fee, markets, mocker) -> None: +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1744,7 +1658,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1771,8 +1684,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, assert trade.close_date is not None -def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1780,7 +1692,6 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) @@ -1825,20 +1736,18 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, def test_handle_trade_roi(default_conf, ticker, limit_buy_order, - fee, mocker, markets, caplog) -> None: + fee, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) @@ -1859,20 +1768,18 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, def test_handle_trade_use_sell_signal( - default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: + default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None: # use_sell_signal is True buy default caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) - patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) - freqtrade = FreqtradeBot(default_conf) + freqtrade = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.create_trades() @@ -1890,7 +1797,7 @@ def test_handle_trade_use_sell_signal( def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1898,7 +1805,6 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1920,7 +1826,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', @@ -2205,6 +2111,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) @@ -2228,15 +2157,15 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: assert cancel_order_mock.call_count == 1 -def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: +def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2274,15 +2203,15 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc } == last_msg -def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: +def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2322,16 +2251,15 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, - ticker_sell_down, - markets, mocker) -> None: + ticker_sell_down, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2377,8 +2305,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe } == last_msg -def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, - markets, caplog) -> None: +def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) sellmock = MagicMock() @@ -2387,7 +2314,6 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), sell=sellmock ) @@ -2407,9 +2333,8 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, assert log_has('Could not cancel stoploss order abcd', caplog) -def test_execute_sell_with_stoploss_on_exchange(default_conf, - ticker, fee, ticker_sell_up, - markets, mocker) -> None: +def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, + mocker) -> None: default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) @@ -2426,7 +2351,6 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, stoploss_limit=stoploss_limit, @@ -2461,10 +2385,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, assert rpc_mock.call_count == 2 -def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, - ticker, fee, - limit_buy_order, - markets, mocker) -> None: +def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, + limit_buy_order, mocker) -> None: default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -2472,7 +2394,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets), symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, ) @@ -2529,125 +2450,16 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, - ticker, fee, - limit_buy_order, - markets, mocker) -> None: - """ - Tests workflow of selling stoploss_on_exchange. - Sells - * first trade as stoploss - * 2nd trade is kept - * 3rd trade is sold via sell-signal - """ - default_conf['max_open_trades'] = 3 - default_conf['exchange']['name'] = 'binance' - patch_RPCManager(mocker) - patch_exchange(mocker) - - stoploss_limit = { - 'id': 123, - 'info': {} - } - stoploss_order_open = { - "id": "123", - "timestamp": 1542707426845, - "datetime": "2018-11-20T09:50:26.845Z", - "lastTradeTimestamp": None, - "symbol": "BTC/USDT", - "type": "stop_loss_limit", - "side": "sell", - "price": 1.08801, - "amount": 90.99181074, - "cost": 0.0, - "average": 0.0, - "filled": 0.0, - "remaining": 0.0, - "status": "open", - "fee": None, - "trades": None - } - stoploss_order_closed = stoploss_order_open.copy() - stoploss_order_closed['status'] = 'closed' - # Sell first trade based on stoploss, keep 2nd and 3rd trade open - stoploss_order_mock = MagicMock( - side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) - # Sell 3rd trade (not called for the first trade) - should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] - ) - cancel_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets), - symbol_amount_prec=lambda s, x, y: y, - symbol_price_prec=lambda s, x, y: y, - get_order=stoploss_order_mock, - cancel_order=cancel_order_mock, - ) - - wallets_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.freqtradebot.FreqtradeBot', - create_stoploss_order=MagicMock(return_value=True), - update_trade_state=MagicMock(), - _notify_sell=MagicMock(), - ) - mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) - mocker.patch("freqtrade.wallets.Wallets.update", wallets_mock) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - # Switch ordertype to market to close trade immediately - freqtrade.strategy.order_types['sell'] = 'market' - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trades() - wallets_mock.reset_mock() - Trade.session = MagicMock() - - trades = Trade.query.all() - # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) - for trade in trades: - trade.stoploss_order_id = 3 - trade.open_order_id = None - - freqtrade.process_maybe_execute_sells(trades) - assert should_sell_mock.call_count == 2 - - # Only order for 3rd trade needs to be cancelled - assert cancel_order_mock.call_count == 1 - # Wallets should only be called once per sell cycle - assert wallets_mock.call_count == 1 - - trade = trades[0] - assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value - assert not trade.is_open - - trade = trades[1] - assert not trade.sell_reason - assert trade.is_open - - trade = trades[2] - assert trade.sell_reason == SellType.SELL_SIGNAL.value - assert not trade.is_open - - def test_execute_sell_market_order(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: + ticker_sell_up, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2691,7 +2503,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2703,7 +2515,6 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2723,7 +2534,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2735,7 +2546,6 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2753,7 +2563,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2765,7 +2575,6 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2783,7 +2592,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market assert freqtrade.handle_trade(trade) is False -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2795,7 +2604,6 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'use_sell_signal': True, @@ -2815,14 +2623,13 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mocker, caplog) -> None: +def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2852,7 +2659,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock assert log_has(f"Pair {trade.pair} is currently locked.", caplog) -def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2864,7 +2671,6 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'ignore_roi_if_buy_signal': True @@ -2886,7 +2692,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m assert trade.sell_reason == SellType.ROI.value -def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, mocker) -> None: +def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2898,9 +2704,9 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) default_conf['trailing_stop'] = True + patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) @@ -2938,7 +2744,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets, +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) @@ -2952,10 +2758,11 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.01 + patch_whitelist(mocker, default_conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) @@ -2995,7 +2802,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, - caplog, mocker, markets) -> None: + caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) patch_exchange(mocker) @@ -3008,9 +2815,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) - + patch_whitelist(mocker, default_conf) default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.01 default_conf['trailing_stop_positive_offset'] = 0.011 @@ -3055,7 +2861,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, - caplog, mocker, markets) -> None: + caplog, mocker) -> None: buy_price = limit_buy_order['price'] # buy_price: 0.00001099 @@ -3070,9 +2876,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets), ) - + patch_whitelist(mocker, default_conf) default_conf['trailing_stop'] = True default_conf['trailing_stop_positive'] = 0.05 default_conf['trailing_stop_positive_offset'] = 0.055 @@ -3120,7 +2925,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, - fee, markets, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3132,7 +2937,6 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) default_conf['ask_strategy'] = { 'ignore_roi_if_buy_signal': False @@ -3427,7 +3231,7 @@ def test_get_real_amount_open_trade(default_conf, mocker): assert freqtrade.get_real_amount(trade, order) == amount -def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, markets, mocker, +def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1 @@ -3439,7 +3243,6 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) # Save state of current whitelist @@ -3455,6 +3258,8 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, assert trade.open_date is not None assert trade.exchange == 'bittrex' + assert len(Trade.query.all()) == 1 + # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -3463,7 +3268,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order, - fee, markets, mocker, order_book_l2): + fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True # delta is 100 which is impossible to reach. hence check_depth_of_market will return false default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100 @@ -3475,7 +3280,6 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) # Save state of current whitelist freqtrade = FreqtradeBot(default_conf) @@ -3486,7 +3290,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) -> None: +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: """ test if function get_target_bid will return the order book price instead of the ask rate @@ -3495,7 +3299,6 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_order_book=order_book_l2, get_ticker=ticker_mock, @@ -3511,7 +3314,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) assert ticker_mock.call_count == 0 -def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None: +def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None: """ test if function get_target_bid will return the ask rate (since its value is lower) instead of the order book rate (even if enabled) @@ -3520,7 +3323,6 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_order_book=order_book_l2, get_ticker=ticker_mock, @@ -3537,14 +3339,13 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) assert ticker_mock.call_count == 0 -def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None: +def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: """ test check depth of market """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), get_order_book=order_book_l2 ) default_conf['telegram']['enabled'] = False @@ -3559,7 +3360,7 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order, - fee, markets, mocker, order_book_l2) -> None: + fee, mocker, order_book_l2) -> None: """ test order book ask strategy """ @@ -3581,7 +3382,6 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - markets=PropertyMock(return_value=markets) ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 000000000..228ed8468 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,159 @@ + +from unittest.mock import MagicMock + +from freqtrade.persistence import Trade +from freqtrade.strategy.interface import SellCheckTuple, SellType +from tests.conftest import get_patched_freqtradebot, patch_get_signal +from freqtrade.rpc.rpc import RPC + + +def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, + limit_buy_order, mocker) -> None: + """ + Tests workflow of selling stoploss_on_exchange. + Sells + * first trade as stoploss + * 2nd trade is kept + * 3rd trade is sold via sell-signal + """ + default_conf['max_open_trades'] = 3 + default_conf['exchange']['name'] = 'binance' + + stoploss_limit = { + 'id': 123, + 'info': {} + } + stoploss_order_open = { + "id": "123", + "timestamp": 1542707426845, + "datetime": "2018-11-20T09:50:26.845Z", + "lastTradeTimestamp": None, + "symbol": "BTC/USDT", + "type": "stop_loss_limit", + "side": "sell", + "price": 1.08801, + "amount": 90.99181074, + "cost": 0.0, + "average": 0.0, + "filled": 0.0, + "remaining": 0.0, + "status": "open", + "fee": None, + "trades": None + } + stoploss_order_closed = stoploss_order_open.copy() + stoploss_order_closed['status'] = 'closed' + # Sell first trade based on stoploss, keep 2nd and 3rd trade open + stoploss_order_mock = MagicMock( + side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) + # Sell 3rd trade (not called for the first trade) + should_sell_mock = MagicMock(side_effect=[ + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] + ) + cancel_order_mock = MagicMock() + mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + get_fee=fee, + symbol_amount_prec=lambda s, x, y: y, + symbol_price_prec=lambda s, x, y: y, + get_order=stoploss_order_mock, + cancel_order=cancel_order_mock, + ) + + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + create_stoploss_order=MagicMock(return_value=True), + update_trade_state=MagicMock(), + _notify_sell=MagicMock(), + ) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) + wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + # Switch ordertype to market to close trade immediately + freqtrade.strategy.order_types['sell'] = 'market' + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trades() + wallets_mock.reset_mock() + Trade.session = MagicMock() + + trades = Trade.query.all() + # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) + for trade in trades: + trade.stoploss_order_id = 3 + trade.open_order_id = None + + freqtrade.process_maybe_execute_sells(trades) + assert should_sell_mock.call_count == 2 + + # Only order for 3rd trade needs to be cancelled + assert cancel_order_mock.call_count == 1 + # Wallets should only be called once per sell cycle + assert wallets_mock.call_count == 1 + + trade = trades[0] + assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value + assert not trade.is_open + + trade = trades[1] + assert not trade.sell_reason + assert trade.is_open + + trade = trades[2] + assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert not trade.is_open + + +def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker) -> None: + """ + Tests workflow + """ + default_conf['max_open_trades'] = 5 + default_conf['forcebuy_enable'] = True + default_conf['stake_amount'] = 'unlimited' + default_conf['exchange']['name'] = 'binance' + default_conf['telegram']['enabled'] = True + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock( + side_effect=[1000, 800, 600, 400, 200] + )) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + get_fee=fee, + symbol_amount_prec=lambda s, x, y: y, + symbol_price_prec=lambda s, x, y: y, + ) + + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + create_stoploss_order=MagicMock(return_value=True), + update_trade_state=MagicMock(), + _notify_sell=MagicMock(), + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtrade) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + # Switch ordertype to market to close trade immediately + freqtrade.strategy.order_types['sell'] = 'market' + patch_get_signal(freqtrade) + + # Create 4 trades + freqtrade.create_trades() + + trades = Trade.query.all() + assert len(trades) == 4 + rpc._rpc_forcebuy('TKN/BTC', None) + + trades = Trade.query.all() + assert len(trades) == 5 + + for trade in trades: + assert trade.stake_amount == 200 diff --git a/tests/test_main.py b/tests/test_main.py index d73edc0da..4e97c375d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6bd223a9b..231a1d2e2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -35,6 +35,8 @@ def create_mock_trades(fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, + close_rate=0.128, + close_profit=0.005, exchange='bittrex', is_open=False, open_order_id='dry_run_sell_12345' @@ -59,7 +61,7 @@ def test_init_create_session(default_conf): # Check if init create a session init(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') - assert 'Session' in type(Trade.session).__name__ + assert 'scoped_session' in type(Trade.session).__name__ def test_init_custom_db_url(default_conf, mocker): @@ -835,3 +837,38 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.stop_loss_pct == -0.04 assert trade_adj.initial_stop_loss == 0.96 assert trade_adj.initial_stop_loss_pct == -0.04 + + +@pytest.mark.usefixtures("init_persistence") +def test_total_open_trades_stakes(fee): + + res = Trade.total_open_trades_stakes() + assert res == 0 + create_mock_trades(fee) + res = Trade.total_open_trades_stakes() + assert res == 0.002 + + +@pytest.mark.usefixtures("init_persistence") +def test_get_overall_performance(fee): + + create_mock_trades(fee) + res = Trade.get_overall_performance() + + assert len(res) == 1 + assert 'pair' in res[0] + assert 'profit' in res[0] + assert 'count' in res[0] + + +@pytest.mark.usefixtures("init_persistence") +def test_get_best_pair(fee): + + res = Trade.get_best_pair() + assert res is None + + create_mock_trades(fee) + res = Trade.get_best_pair() + assert len(res) == 2 + assert res[0] == 'ETC/BTC' + assert res[1] == 0.005 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a39b2b76e..31502cafc 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -53,10 +53,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir): assert "trades" in ret assert "pairs" in ret - default_conf['pairs'] = ["POWR/BTC", "ADA/BTC"] + default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] ret = init_plotscript(default_conf) assert "tickers" in ret - assert "POWR/BTC" in ret["tickers"] + assert "TRX/BTC" in ret["tickers"] assert "ADA/BTC" in ret["tickers"] @@ -64,7 +64,7 @@ def test_add_indicators(default_conf, testdatadir, caplog): pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) - data = history.load_pair_history(pair=pair, ticker_interval='1m', + data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) indicators1 = ["ema10"] indicators2 = ["macd"] @@ -129,7 +129,7 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t pair = "UNITTEST/BTC" timerange = TimeRange(None, 'line', 0, -1000) - data = history.load_pair_history(pair=pair, ticker_interval='1m', + data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) data['buy'] = 0 data['sell'] = 0 @@ -164,7 +164,7 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) MagicMock(side_effect=fig_generating_mock)) pair = 'UNITTEST/BTC' timerange = TimeRange(None, 'line', 0, -1000) - data = history.load_pair_history(pair=pair, ticker_interval='1m', + data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) # Generate buy/sell signals and indicators @@ -212,9 +212,9 @@ def test_generate_plot_file(mocker, caplog): fig = generate_empty_figure() plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html", - directory=Path("user_data/plots")) + directory=Path("user_data/plot")) - expected_fn = str(Path("user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")) + expected_fn = str(Path("user_data/plot/freqtrade-plot-UNITTEST_BTC-5m.html")) assert plot_mock.call_count == 1 assert plot_mock.call_args[0][0] == fig assert (plot_mock.call_args_list[0][1]['filename'] @@ -228,13 +228,13 @@ def test_add_profit(testdatadir): bt_data = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m', + df = history.load_pair_history(pair="TRX/BTC", timeframe='5m', datadir=testdatadir, timerange=timerange) fig = generate_empty_figure() cum_profits = create_cum_profit(df.set_index('date'), - bt_data[bt_data["pair"] == 'POWR/BTC'], - "cum_profits") + bt_data[bt_data["pair"] == 'TRX/BTC'], + "cum_profits", timeframe="5m") fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits') figure = fig1.layout.figure @@ -247,16 +247,16 @@ def test_generate_profit_graph(testdatadir): filename = testdatadir / "backtest-result_test.json" trades = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") - pairs = ["POWR/BTC", "ADA/BTC"] + pairs = ["TRX/BTC", "ADA/BTC"] tickers = history.load_data(datadir=testdatadir, pairs=pairs, - ticker_interval='5m', + timeframe='5m', timerange=timerange ) trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, tickers, trades) + fig = generate_profit_graph(pairs, tickers, trades, timeframe="5m") assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" @@ -281,8 +281,8 @@ def test_generate_profit_graph(testdatadir): def test_start_plot_dataframe(mocker): aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock()) args = [ - "--config", "config.json.example", "plot-dataframe", + "--config", "config.json.example", "--pairs", "ETH/BTC" ] start_plot_dataframe(get_args(args)) @@ -323,8 +323,8 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): def test_start_plot_profit(mocker): aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock()) args = [ - "--config", "config.json.example", "plot-profit", + "--config", "config.json.example", "--pairs", "ETH/BTC" ] start_plot_profit(get_args(args)) diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 4851cbebd..5c35535f0 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -1,10 +1,11 @@ # pragma pylint: disable=missing-docstring, C0103 +import arrow import pytest from freqtrade.configuration import TimeRange -def test_parse_timerange_incorrect() -> None: +def test_parse_timerange_incorrect(): assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-') assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522') @@ -28,3 +29,37 @@ def test_parse_timerange_incorrect() -> None: with pytest.raises(Exception, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') + + +def test_subtract_start(): + x = TimeRange('date', 'date', 1274486400, 1438214400) + x.subtract_start(300) + assert x.startts == 1274486400 - 300 + + # Do nothing if no startdate exists + x = TimeRange(None, 'date', 0, 1438214400) + x.subtract_start(300) + assert not x.startts + + x = TimeRange('date', None, 1274486400, 0) + x.subtract_start(300) + assert x.startts == 1274486400 - 300 + + +def test_adjust_start_if_necessary(): + min_date = arrow.Arrow(2017, 11, 14, 21, 15, 00) + + x = TimeRange('date', 'date', 1510694100, 1510780500) + # Adjust by 20 candles - min_date == startts + x.adjust_start_if_necessary(300, 20, min_date) + assert x.startts == 1510694100 + (20 * 300) + + x = TimeRange('date', 'date', 1510700100, 1510780500) + # Do nothing, startup is set and different min_date + x.adjust_start_if_necessary(300, 20, min_date) + assert x.startts == 1510694100 + (20 * 300) + + x = TimeRange(None, 'date', 0, 1510780500) + # Adjust by 20 candles = 20 * 5m + x.adjust_start_if_necessary(300, 20, min_date) + assert x.startts == 1510694100 + (20 * 300) diff --git a/tests/test_utils.py b/tests/test_utils.py index f64a6924a..1258c939c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,22 +8,47 @@ from freqtrade import OperationalException from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, - start_list_markets, start_list_timeframes) -from tests.conftest import get_args, log_has, patch_exchange + start_list_markets, start_list_timeframes, + start_new_hyperopt, start_new_strategy, + start_trading) +from tests.conftest import get_args, log_has, log_has_re, patch_exchange def test_setup_utils_configuration(): args = [ - '--config', 'config.json.example', + 'list-exchanges', '--config', 'config.json.example', ] config = setup_utils_configuration(get_args(args), RunMode.OTHER) assert "exchange" in config - assert config['exchange']['dry_run'] is True + assert config['dry_run'] is True assert config['exchange']['key'] == '' assert config['exchange']['secret'] == '' +def test_start_trading_fail(mocker): + + mocker.patch("freqtrade.worker.Worker.run", MagicMock(side_effect=OperationalException)) + + mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(return_value=None)) + + exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock()) + args = [ + 'trade', + '-c', 'config.json.example' + ] + with pytest.raises(OperationalException): + start_trading(get_args(args)) + assert exitmock.call_count == 1 + + exitmock.reset_mock() + + mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException)) + with pytest.raises(OperationalException): + start_trading(get_args(args)) + assert exitmock.call_count == 0 + + def test_list_exchanges(capsys): args = [ @@ -95,8 +120,8 @@ def test_list_timeframes(mocker, capsys): # Test with --config config.json.example args = [ - '--config', 'config.json.example', "list-timeframes", + '--config', 'config.json.example', ] start_list_timeframes(get_args(args)) captured = capsys.readouterr() @@ -139,8 +164,8 @@ def test_list_timeframes(mocker, capsys): # Test with --one-column args = [ - '--config', 'config.json.example', "list-timeframes", + '--config', 'config.json.example', "--one-column", ] start_list_timeframes(get_args(args)) @@ -182,14 +207,14 @@ def test_list_markets(mocker, markets, capsys): # Test with --config config.json.example args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 8 active markets: " - "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC, XLTCUSDT.\n" + assert ("Exchange Bittrex has 9 active markets: " + "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n" in captured.out) patch_exchange(mocker, api_mock=api_mock, id="binance") @@ -202,14 +227,14 @@ def test_list_markets(mocker, markets, capsys): pargs['config'] = None start_list_markets(pargs, False) captured = capsys.readouterr() - assert re.match("\nExchange Binance has 8 active markets:\n", + assert re.match("\nExchange Binance has 9 active markets:\n", captured.out) patch_exchange(mocker, api_mock=api_mock, id="bittrex") # Test with --all: all markets args = [ - '--config', 'config.json.example', "list-markets", "--all", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), False) @@ -221,20 +246,20 @@ def test_list_markets(mocker, markets, capsys): # Test list-pairs subcommand: active pairs args = [ - '--config', 'config.json.example', "list-pairs", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), True) captured = capsys.readouterr() - assert ("Exchange Bittrex has 7 active pairs: " - "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC.\n" + assert ("Exchange Bittrex has 8 active pairs: " + "BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, NEO/BTC, TKN/BTC, XRP/BTC.\n" in captured.out) # Test list-pairs subcommand with --all: all pairs args = [ - '--config', 'config.json.example', "list-pairs", "--all", + '--config', 'config.json.example', "--print-list", ] start_list_markets(get_args(args), True) @@ -246,99 +271,99 @@ def test_list_markets(mocker, markets, capsys): # active markets, base=ETH, LTC args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "ETH", "LTC", "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() assert ("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies: " - "ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, XLTCUSDT.\n" + "ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, XLTCUSDT.\n" in captured.out) # active markets, base=LTC args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() assert ("Exchange Bittrex has 3 active markets with LTC as base currency: " - "LTC/USD, LTC/USDT, XLTCUSDT.\n" + "LTC/BTC, LTC/USD, XLTCUSDT.\n" in captured.out) # active markets, quote=USDT, USD args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--quote", "USDT", "USD", "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 4 active markets with USDT, USD as quote currencies: " - "ETH/USDT, LTC/USD, LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 3 active markets with USDT, USD as quote currencies: " + "ETH/USDT, LTC/USD, XLTCUSDT.\n" in captured.out) # active markets, quote=USDT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--quote", "USDT", "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 3 active markets with USDT as quote currency: " - "ETH/USDT, LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 2 active markets with USDT as quote currency: " + "ETH/USDT, XLTCUSDT.\n" in captured.out) # active markets, base=LTC, quote=USDT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "USDT", "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 2 active markets with LTC as base currency and " - "with USDT as quote currency: LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 1 active market with LTC as base currency and " + "with USDT as quote currency: XLTCUSDT.\n" in captured.out) # active pairs, base=LTC, quote=USDT args = [ - '--config', 'config.json.example', "list-pairs", - "--base", "LTC", "--quote", "USDT", + '--config', 'config.json.example', + "--base", "LTC", "--quote", "USD", "--print-list", ] start_list_markets(get_args(args), True) captured = capsys.readouterr() assert ("Exchange Bittrex has 1 active pair with LTC as base currency and " - "with USDT as quote currency: LTC/USDT.\n" + "with USD as quote currency: LTC/USD.\n" in captured.out) # active markets, base=LTC, quote=USDT, NONEXISTENT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "USDT", "NONEXISTENT", "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 2 active markets with LTC as base currency and " - "with USDT, NONEXISTENT as quote currencies: LTC/USDT, XLTCUSDT.\n" + assert ("Exchange Bittrex has 1 active market with LTC as base currency and " + "with USDT, NONEXISTENT as quote currencies: XLTCUSDT.\n" in captured.out) # active markets, base=LTC, quote=NONEXISTENT args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "NONEXISTENT", "--print-list", ] @@ -350,18 +375,18 @@ def test_list_markets(mocker, markets, capsys): # Test tabular output args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Exchange Bittrex has 8 active markets:\n" + assert ("Exchange Bittrex has 9 active markets:\n" in captured.out) # Test tabular output, no markets found args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--base", "LTC", "--quote", "NONEXISTENT", ] start_list_markets(get_args(args), False) @@ -372,37 +397,38 @@ def test_list_markets(mocker, markets, capsys): # Test --print-json args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--print-json" ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ('["BLK/BTC","BTT/BTC","ETH/BTC","ETH/USDT","LTC/USD","LTC/USDT","TKN/BTC","XLTCUSDT"]' + assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/USD","NEO/BTC",' + '"TKN/BTC","XLTCUSDT","XRP/BTC"]' in captured.out) # Test --print-csv args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--print-csv" ] start_list_markets(get_args(args), False) captured = capsys.readouterr() assert ("Id,Symbol,Base,Quote,Active,Is pair" in captured.out) assert ("blkbtc,BLK/BTC,BLK,BTC,True,True" in captured.out) - assert ("BTTBTC,BTT/BTC,BTT,BTC,True,True" in captured.out) + assert ("USD-LTC,LTC/USD,LTC,USD,True,True" in captured.out) # Test --one-column args = [ - '--config', 'config.json.example', "list-markets", + '--config', 'config.json.example', "--one-column" ] start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) - assert re.search(r"^BTT/BTC$", captured.out, re.MULTILINE) + assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) def test_create_datadir_failed(caplog): @@ -417,6 +443,7 @@ def test_create_datadir_failed(caplog): def test_create_datadir(caplog, mocker): cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) + csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ "create-userdir", "--userdir", @@ -425,9 +452,82 @@ def test_create_datadir(caplog, mocker): start_create_userdir(get_args(args)) assert cud.call_count == 1 + assert csf.call_count == 1 assert len(caplog.record_tuples) == 0 +def test_start_new_strategy(mocker, caplog): + wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + args = [ + "new-strategy", + "--strategy", + "CoolNewStrategy" + ] + start_new_strategy(get_args(args)) + + assert wt_mock.call_count == 1 + assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0] + assert log_has_re("Writing strategy to .*", caplog) + + +def test_start_new_strategy_DefaultStrat(mocker, caplog): + args = [ + "new-strategy", + "--strategy", + "DefaultStrategy" + ] + with pytest.raises(OperationalException, + match=r"DefaultStrategy is not allowed as name\."): + start_new_strategy(get_args(args)) + + +def test_start_new_strategy_no_arg(mocker, caplog): + args = [ + "new-strategy", + ] + with pytest.raises(OperationalException, + match="`new-strategy` requires --strategy to be set."): + start_new_strategy(get_args(args)) + + +def test_start_new_hyperopt(mocker, caplog): + wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + + args = [ + "new-hyperopt", + "--hyperopt", + "CoolNewhyperopt" + ] + start_new_hyperopt(get_args(args)) + + assert wt_mock.call_count == 1 + assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] + assert log_has_re("Writing hyperopt to .*", caplog) + + +def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): + args = [ + "new-hyperopt", + "--hyperopt", + "DefaultHyperopt" + ] + with pytest.raises(OperationalException, + match=r"DefaultHyperopt is not allowed as name\."): + start_new_hyperopt(get_args(args)) + + +def test_start_new_hyperopt_no_arg(mocker, caplog): + args = [ + "new-hyperopt", + ] + with pytest.raises(OperationalException, + match="`new-hyperopt` requires --hyperopt to be set."): + start_new_hyperopt(get_args(args)) + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) diff --git a/tests/test_worker.py b/tests/test_worker.py new file mode 100644 index 000000000..72e215210 --- /dev/null +++ b/tests/test_worker.py @@ -0,0 +1,81 @@ +import logging +import time +from unittest.mock import MagicMock, PropertyMock + +from freqtrade.data.dataprovider import DataProvider +from freqtrade.state import State +from freqtrade.worker import Worker +from tests.conftest import get_patched_worker, log_has + + +def test_worker_state(mocker, default_conf, markets) -> None: + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + worker = get_patched_worker(mocker, default_conf) + assert worker.state is State.RUNNING + + default_conf.pop('initial_state') + worker = Worker(args=None, config=default_conf) + assert worker.state is State.STOPPED + + +def test_worker_running(mocker, default_conf, caplog) -> None: + mock_throttle = MagicMock() + mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) + mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock()) + + worker = get_patched_worker(mocker, default_conf) + + state = worker._worker(old_state=None) + assert state is State.RUNNING + assert log_has('Changing state to: RUNNING', caplog) + assert mock_throttle.call_count == 1 + # Check strategy is loaded, and received a dataprovider object + assert worker.freqtrade.strategy + assert worker.freqtrade.strategy.dp + assert isinstance(worker.freqtrade.strategy.dp, DataProvider) + + +def test_worker_stopped(mocker, default_conf, caplog) -> None: + mock_throttle = MagicMock() + mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle) + mock_sleep = mocker.patch('time.sleep', return_value=None) + + worker = get_patched_worker(mocker, default_conf) + worker.state = State.STOPPED + state = worker._worker(old_state=State.RUNNING) + assert state is State.STOPPED + assert log_has('Changing state to: STOPPED', caplog) + assert mock_throttle.call_count == 0 + assert mock_sleep.call_count == 1 + + +def test_throttle(mocker, default_conf, caplog) -> None: + def throttled_func(): + return 42 + + caplog.set_level(logging.DEBUG) + worker = get_patched_worker(mocker, default_conf) + + start = time.time() + result = worker._throttle(throttled_func, min_secs=0.1) + end = time.time() + + assert result == 42 + assert end - start > 0.1 + assert log_has('Throttling throttled_func for 0.10 seconds', caplog) + + result = worker._throttle(throttled_func, min_secs=-1) + assert result == 42 + + +def test_throttle_with_assets(mocker, default_conf) -> None: + def throttled_func(nb_assets=-1): + return nb_assets + + worker = get_patched_worker(mocker, default_conf) + + result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666) + assert result == 666 + + result = worker._throttle(throttled_func, min_secs=0.1) + assert result == -1 diff --git a/tests/testdata/POWR_BTC-5m.json b/tests/testdata/TRX_BTC-5m.json similarity index 100% rename from tests/testdata/POWR_BTC-5m.json rename to tests/testdata/TRX_BTC-5m.json diff --git a/tests/testdata/backtest-result_test.json b/tests/testdata/backtest-result_test.json index 8701451dc..dce22acaf 100644 --- a/tests/testdata/backtest-result_test.json +++ b/tests/testdata/backtest-result_test.json @@ -1 +1 @@ -[["POWR/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["POWR/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["POWR/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["POWR/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["POWR/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["POWR/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["POWR/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["POWR/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["POWR/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["POWR/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["POWR/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["POWR/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["POWR/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["POWR/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["POWR/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] \ No newline at end of file +[["TRX/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["TRX/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["TRX/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["TRX/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["TRX/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["TRX/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["TRX/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["TRX/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["TRX/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["TRX/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["TRX/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["TRX/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["TRX/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["TRX/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["TRX/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] diff --git a/tests/testdata/pairs.json b/tests/testdata/pairs.json index f4bab6dc5..15aae2643 100644 --- a/tests/testdata/pairs.json +++ b/tests/testdata/pairs.json @@ -9,7 +9,7 @@ "LTC/BTC", "NEO/BTC", "NXT/BTC", - "POWR/BTC", + "TRX/BTC", "STORJ/BTC", "QTUM/BTC", "WAVES/BTC", diff --git a/user_data/hyperopts/__init__.py b/user_data/hyperopts/.gitkeep similarity index 100% rename from user_data/hyperopts/__init__.py rename to user_data/hyperopts/.gitkeep diff --git a/user_data/strategies/__init__.py b/user_data/strategies/.gitkeep similarity index 100% rename from user_data/strategies/__init__.py rename to user_data/strategies/.gitkeep