diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 19e09c969..6389a50df 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,20 +1,21 @@ FROM freqtradeorg/freqtrade:develop +USER root # Install dependencies COPY requirements-dev.txt /freqtrade/ + RUN apt-get update \ - && apt-get -y install git mercurial sudo vim \ + && apt-get -y install git mercurial sudo vim build-essential \ && apt-get clean \ - && pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \ - && useradd -u 1000 -U -m ftuser \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ - && mv /root/.local /home/ftuser/.local/ \ && chown ftuser:ftuser -R /home/ftuser/.local/ \ && chown ftuser: -R /home/ftuser/ USER ftuser +RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir + # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/.gitattributes b/.gitattributes index bc0382636..81f6f422e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ -* encoding=UTF-8 *.py eol=lf *.sh eol=lf *.ps1 eol=crlf diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..7591c1fb0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +--- +blank_issues_enabled: false +contact_links: + - name: Discord Server + url: https://discord.gg/MA9v74M + about: Ask a question or get community support from our Discord server diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4169661c6..5a0837eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.3.2 + uses: rokroskar/workflow-run-cleanup-action@v0.3.3 if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/Dockerfile b/Dockerfile index 128d0e19c..7d5afac9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.4-slim-buster as base +FROM python:3.9.5-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/README.md b/README.md index 916f9cf17..ab9597a77 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![freqtrade](docs/assets/freqtrade_poweredby.svg) +# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) @@ -154,7 +154,7 @@ You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/ If you discover a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) first. If it hasn't been reported, please -[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and +[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and ensure you follow the template guide so that our team can assist you as quickly as possible. @@ -163,7 +163,7 @@ quickly as possible. Have you a great idea to improve the bot you want to share? Please, first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). If it hasn't been requested, please -[create a new request](https://github.com/freqtrade/freqtrade/issues/new) +[create a new request](https://github.com/freqtrade/freqtrade/issues/new/choose) and ensure you follow the template guide so that it does not get lost in the bug reports. diff --git a/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl deleted file mode 100644 index 5adbda8a7..000000000 Binary files a/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl deleted file mode 100644 index b652c7ee0..000000000 Binary files a/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl new file mode 100644 index 000000000..b4eee9c47 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.20-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.20-cp38-cp38-win_amd64.whl new file mode 100644 index 000000000..de7393fde Binary files /dev/null and b/build_helpers/TA_Lib-0.4.20-cp38-cp38-win_amd64.whl differ diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index cb86e5f64..dd87cf105 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -8,10 +8,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ + && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \ + && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ - && make \ + && make -j$(nproc) \ && which sudo && sudo make install || make install \ && cd .. else echo "TA-lib already installed, skipping installation" fi +# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 5747db335..d2a27dd26 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,16 +1,15 @@ # 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" python -m pip install --upgrade pip $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" if ($pyv -eq '3.7') { - pip install build_helpers\TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl } if ($pyv -eq '3.8') { - pip install build_helpers\TA_Lib-0.4.19-cp38-cp38-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.20-cp38-cp38-win_amd64.whl } pip install -r requirements-dev.txt diff --git a/config_full.json.example b/config_full.json.example index 973afe2c8..6aeb756f3 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -23,7 +23,8 @@ "stoploss": -0.10, "unfilledtimeout": { "buy": 10, - "sell": 30 + "sell": 30, + "unit": "minutes" }, "bid_strategy": { "price_side": "bid", @@ -164,7 +165,16 @@ "startup": "on", "buy": "on", "buy_fill": "on", - "sell": "on", + "sell": { + "roi": "off", + "emergency_sell": "off", + "force_sell": "off", + "sell_signal": "off", + "trailing_stop_loss": "off", + "stop_loss": "off", + "stoploss_on_exchange": "off", + "custom_sell": "off" + }, "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 new file mode 100644 index 000000000..71c10d949 --- /dev/null +++ b/docker/Dockerfile.aarch64 @@ -0,0 +1,58 @@ +FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base + +# Setup env +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONFAULTHANDLER 1 +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" + +# Prepare environment +RUN mkdir /freqtrade \ + && apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers + +WORKDIR /freqtrade + +# Install dependencies +FROM base as python-deps +RUN apt-get update \ + && apt-get -y install curl build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ + && apt-get clean \ + && pip install --upgrade pip + +# Install TA-lib +COPY build_helpers/* /tmp/ +RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* +ENV LD_LIBRARY_PATH /usr/local/lib + +# Install dependencies +COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ +USER ftuser +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-cache-dir -r requirements-hyperopt.txt + +# Copy dependencies to runtime-image +FROM base as runtime-image +COPY --from=python-deps /usr/local/lib /usr/local/lib +ENV LD_LIBRARY_PATH /usr/local/lib + +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local + +USER ftuser +# Install and execute +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + +ENTRYPOINT ["freqtrade"] +# Default to trade mode +CMD [ "trade" ] diff --git a/docs/assets/telegram_forcebuy.png b/docs/assets/telegram_forcebuy.png new file mode 100644 index 000000000..b0592bff3 Binary files /dev/null and b/docs/assets/telegram_forcebuy.png differ diff --git a/docs/backtesting.md b/docs/backtesting.md index ee9926f32..2027c2079 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -237,29 +237,29 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -========================================================= BACKTESTING REPORT ======================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:| -| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 0 | 21 | -| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 0 | 8 | -| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 0 | 14 | -| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 0 | 7 | -| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 0 | 10 | -| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 0 | 20 | -| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 0 | 15 | -| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 0 | 17 | -| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 0 | 18 | -| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 0 | 9 | -| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 0 | 21 | -| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 0 | 7 | -| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 0 | 13 | -| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 0 | 5 | -| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 0 | 9 | -| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 0 | 11 | -| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 0 | 23 | -| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 0 | 15 | -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | -========================================================= SELL REASON STATS ========================================================= +========================================================= BACKTESTING REPORT ========================================================== +| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | +|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| +| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | +| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | +| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | +| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | +| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | +| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | +| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | +| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | +| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | +| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | +| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | +| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | +| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | +| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | +| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | +| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | +| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | +| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | +========================================================= SELL REASON STATS ========================================================== | Sell Reason | Sells | Wins | Draws | Losses | |:-------------------|--------:|------:|-------:|--------:| | trailing_stop_loss | 205 | 150 | 0 | 55 | @@ -267,11 +267,11 @@ A backtesting result will look like that: | sell_signal | 56 | 36 | 0 | 20 | | force_sell | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:| -| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 | -| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 | -| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 | +| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | +|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| +| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | +| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | +| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | =============== SUMMARY METRICS =============== | Metric | Value | |-----------------------+---------------------| @@ -297,6 +297,8 @@ A backtesting result will look like that: | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -318,7 +320,7 @@ The last line will give you the overall performance of your strategy, here: ``` -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | ``` The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has @@ -384,6 +386,8 @@ It contains some useful key metrics about performance of your strategy on backte | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -413,6 +417,8 @@ It contains some useful key metrics about performance of your strategy on backte - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. +- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results. +- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. @@ -472,11 +478,11 @@ There will be an additional table comparing win/losses of the different strategi Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. ``` -=========================================================== STRATEGY SUMMARY =========================================================== -| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:| -| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | -| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | +=========================================================== STRATEGY SUMMARY ========================================================================= +| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | +|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| +| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | +| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | ``` ## Next step diff --git a/docs/configuration.md b/docs/configuration.md index 37395c5ee..eff8fe322 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,8 +68,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [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 | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `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 and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**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 and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 5f48782d2..3a85aa885 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -48,6 +48,8 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Download the docker-compose file from the repository curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml + # Edit the compose file to use an image named `*_pi` (stable_pi or develop_pi) + # Pull the freqtrade image docker-compose pull @@ -65,6 +67,40 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # image: freqtradeorg/freqtrade:develop_pi ``` +=== "ARM 64 Systenms (Mac M1, Raspberry Pi 4, Jetson Nano)" + In case of a Mac M1, make sure that your docker installation is running in native mode + Arm64 images are not yet provided via Docker Hub and need to be build locally first. + Depending on the device, this may take a few minutes (Apple M1) or multiple hours (Raspberry Pi) + + ``` bash + # Clone Freqtrade repository + git clone https://github.com/freqtrade/freqtrade.git + cd freqtrade + # Optionally switch to the stable version + git checkout stable + + # Modify your docker-compose file to enable building and change the image name + # (see the Note Box below for necessary changes) + + # Build image + docker-compose build + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + + !!! Note "Change your docker Image" + You have to change the docker image in the docker-compose file for your arm64 build to work properly. + ``` yml + image: freqtradeorg/freqtrade:custom_arm64 + build: + context: . + dockerfile: "./docker/Dockerfile.aarch64" + ``` + The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 1c95d84c9..dfee27fa2 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -249,19 +249,24 @@ We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): - buy_adx = IntParameter(20, 40, default=30) - buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]), - buy_rsi_enabled = CategoricalParameter([True, False]), - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), + buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy") + buy_rsi = IntParameter(20, 40, default=30, space="buy") + buy_adx_enabled = CategoricalParameter([True, False], default=True, space="buy") + buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy") + buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy") ``` -Above definition says: I have five parameters I want to randomly combine to find the best combination. -Two of them are integer values (`buy_adx` and `buy_rsi`) and I want you test in the range of values 20 to 40. +The above definition says: I have five parameters I want to randomly combine to find the best combination. +`buy_rsi` is an integer parameter, which will be tested between 20 and 40. This space has a size of 20. +`buy_adx` is a decimal parameter, which will be evaluated between 20 and 40 with 1 decimal place (so values are 20.1, 20.2, ...). This space has a size of 200. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. +!!! Note "Parameter space assignment" + Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly. + If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. + So let's write the buy strategy using these values: ```python diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 85d157e75..ce0cc6e57 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -112,6 +112,7 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr * `min_price` * `max_price` +* `max_value` * `low_price_ratio` The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. @@ -120,6 +121,11 @@ This option is disabled by default, and will only apply if set to > 0. The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. This option is disabled by default, and will only apply if set to > 0. +The `max_value` setting removes pairs where the minimum value change is above a specified value. +This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption. +As a result of the above, you can only buy for 20$, or 40$ - but not for 25$. +On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit. + The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to > 0. @@ -193,7 +199,7 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove ### Full example of Pairlist Handlers -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. ```json "exchange": { @@ -204,7 +210,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, { "method": "VolumePairList", "number_assets": 20, - "sort_key": "quoteVolume", + "sort_key": "quoteVolume" }, {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, diff --git a/docs/installation.md b/docs/installation.md index d2661f88f..c19965a18 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces sudo apt-get update # install packages - sudo apt install -y python3-pip python3-venv python3-pandas python3-pip git + sudo apt install -y python3-pip python3-venv python3-pandas git ``` === "RaspberryPi/Raspbian" @@ -269,7 +269,7 @@ git clone https://github.com/freqtrade/freqtrade.git cd freqtrade ``` -#### Freqtrade instal: Conda Environment +#### Freqtrade install: Conda Environment Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index a431b1702..89011272d 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.3 +mkdocs-material==7.1.5 mdx_truly_sane_lists==1.2 -pymdown-extensions==8.1.1 +pymdown-extensions==8.2 diff --git a/docs/rest-api.md b/docs/rest-api.md index a0029a44c..b9b2b29be 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -71,7 +71,10 @@ If you run your bot using docker, you'll need to have the bot listen to incoming "api_server": { "enabled": true, "listen_ip_address": "0.0.0.0", - "listen_port": 8080 + "listen_port": 8080, + "username": "Freqtrader", + "password": "SuperSecret1!", + //... }, ``` @@ -106,7 +109,10 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use "api_server": { "enabled": true, "listen_ip_address": "0.0.0.0", - "listen_port": 8080 + "listen_port": 8080, + "username": "Freqtrader", + "password": "SuperSecret1!", + //... } } ``` diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 569af33ff..477396931 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -19,7 +19,7 @@ The freqtrade docker image does contain sqlite3, so you can edit the database wi ``` bash docker-compose exec freqtrade /bin/bash -sqlite3 .sqlite +sqlite3 .sqlite ``` ## Open the DB @@ -99,3 +99,32 @@ DELETE FROM trades WHERE id = 31; !!! Warning This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause. + +## Use a different database system + +!!! Warning + By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems. + +### PostgreSQL + +Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems. + +Installation: +`pip install psycopg2` + +Usage: +`... --db-url postgresql+psycopg2://:@localhost:5432/` + +Freqtrade will automatically create the tables necessary upon startup. + +If you're running different instances of Freqtrade, you must either setup one database per Instance or use different users / schemas for your connections. + +### MariaDB / MySQL + +Freqtrade supports MariaDB by using SQLAlchemy, which supports multiple different database systems. + +Installation: +`pip install pymysql` + +Usage: +`... --db-url mysql+pymysql://:@localhost:3306/` diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index ccd4c2419..3436604a9 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -40,31 +40,71 @@ class AwesomeStrategy(IStrategy): !!! Note If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. +## Dataframe access + +You may access dataframe in various strategy functions by querying it from dataprovider. + +``` python +from freqtrade.exchange import timeframe_to_prev_date + +class AwesomeStrategy(IStrategy): + def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, + current_time: 'datetime', **kwargs) -> bool: + # Obtain pair dataframe. + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + + # Obtain last available candle. Do not use current_time to look up latest candle, because + # current_time points to curret incomplete candle whose data is not available. + last_candle = dataframe.iloc[-1].squeeze() + # <...> + + # In dry/live runs trade open date will not match candle open date therefore it must be + # rounded. + trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) + # Look up trade candle. + trade_candle = dataframe.loc[dataframe['date'] == trade_date] + # trade_candle may be empty for trades that just opened as it is still incomplete. + if not trade_candle.empty: + trade_candle = trade_candle.squeeze() + # <...> +``` + +!!! Warning "Using .iloc[-1]" + You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see. + This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area. + Also, this will only work starting with version 2021.5. + *** ## Custom sell signal -It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. +It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. + +For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. + +Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. + +!!! Note + Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: ``` python -from freqtrade.strategy import IStrategy, timeframe_to_prev_date - class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, dataframe: DataFrame, **kwargs): - trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) - trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() + current_profit: float, **kwargs): + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() # Above 20% profit, sell when rsi < 80 if current_profit > 0.2: - if trade_row['rsi'] < 80: + if last_candle['rsi'] < 80: return 'rsi_below_80' # Between 2% and 10%, sell if EMA-long above EMA-short if 0.02 < current_profit < 0.1: - if trade_row['emalong'] > trade_row['emashort']: + if last_candle['emalong'] > last_candle['emashort']: return 'ema_long_below_80' # Sell any positions at a loss if they are held for more than one day. @@ -72,7 +112,7 @@ class AwesomeStrategy(IStrategy): return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. ## Custom stoploss @@ -98,8 +138,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -149,8 +188,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -176,8 +214,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -203,8 +240,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -243,8 +279,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -260,59 +295,43 @@ class AwesomeStrategy(IStrategy): #### Custom stoploss using an indicator from dataframe example -Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" - -!!! Warning - Only use `dataframe` values up until and including `current_time` value. Reading past - `current_time` you will look into the future, which will produce incorrect backtesting results - and throw an exception in dry/live runs. - see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. - -!!! Note - `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and - `trade.open_date_utc` will not match the candle date precisely and using them directly will throw - an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date - before using it to access `dataframe`. +Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss. ``` python -from freqtrade.exchange import timeframe_to_prev_date -from freqtrade.persistence import Trade -from freqtrade.state import RunMode - class AwesomeStrategy(IStrategy): - # ... populate_* methods + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # <...> + dataframe['sar'] = ta.SAR(dataframe) use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: - # Default return value - result = 1 - if trade: - # Using current_time directly would only work in backtesting. Live/dry runs need time to - # be rounded to previous candle to be used as dataframe index. Rounding must also be - # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. - current_time = timeframe_to_prev_date(self.timeframe, current_time) - current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() - if 'atr' in current_row: - # new stoploss relative to current_rate - new_stoploss = (current_rate - current_row['atr']) / current_rate - # turn into relative negative offset required by `custom_stoploss` return implementation - result = new_stoploss - 1 + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() - return result + # Use parabolic sar as absolute stoploss price + stoploss_price = last_candle['sar'] + + # Convert absolute price to percentage relative to current_rate + if stoploss_price < current_rate: + return (stoploss_price / current_rate) - 1 + + # return maximum stoploss value, keeping current stoploss price unchanged + return 1 ``` +See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. + --- ## Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. -However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not. +However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not. !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. @@ -538,7 +557,7 @@ Both attributes and methods may be overridden, altering behavior of the original ## Embedding Strategies -Freqtrade provides you with with an easy way to embed the strategy into your configuration file. +Freqtrade provides you 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. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 59bfbde48..cfea60d22 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -159,7 +159,7 @@ Edit the method `populate_buy_trend()` in your strategy file to update your buy It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. -This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". +This method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". Sample from `user_data/strategies/sample_strategy.py`: @@ -193,7 +193,7 @@ Please note that the sell-signal is only used if `use_sell_signal` is set to tru It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. -This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". +This method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". Sample from `user_data/strategies/sample_strategy.py`: @@ -422,10 +422,6 @@ if self.dp: Returns an empty dataframe if the requested pair was not cached. This should not happen when using whitelisted pairs. - -!!! Warning "Warning about backtesting" - This method will return an empty dataframe during backtesting. - ### *orderbook(pair, maximum)* ``` python @@ -631,8 +627,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 824cb17c7..991b5f1fb 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -72,22 +72,31 @@ Example configuration showing the different settings: ``` json "telegram": { - "enabled": true, - "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id", - "notification_settings": { - "status": "silent", - "warning": "on", - "startup": "off", - "buy": "silent", - "sell": "on", - "buy_cancel": "silent", - "sell_cancel": "on", - "buy_fill": "off", - "sell_fill": "off" - }, - "balance_dust_level": 0.01 - }, + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id", + "notification_settings": { + "status": "silent", + "warning": "on", + "startup": "off", + "buy": "silent", + "sell": { + "roi": "silent", + "emergency_sell": "on", + "force_sell": "on", + "sell_signal": "silent", + "trailing_stop_loss": "on", + "stop_loss": "on", + "stoploss_on_exchange": "on", + "custom_sell": "silent" + }, + "buy_cancel": "silent", + "sell_cancel": "on", + "buy_fill": "off", + "sell_fill": "off" + }, + "balance_dust_level": 0.01 +}, ``` `buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. @@ -250,10 +259,14 @@ Return a summary of your profit/loss and performance. > **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` -### /forcebuy +### /forcebuy [rate] > **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) +Omitting the pair will open a query asking for the pair to buy (based on the current whitelist). + +![Telegram force-buy screenshot](assets/telegram_forcebuy.png) + Note that for this to work, `forcebuy_enable` needs to be set to true. [More details](configuration.md#understand-forcebuy_enable) @@ -261,12 +274,12 @@ Note that for this to work, `forcebuy_enable` needs to be set to true. ### /performance Return the performance of each crypto-currency the bot has sold. -> Performance: -> 1. `RCN/BTC 57.77%` -> 2. `PAY/BTC 56.91%` -> 3. `VIB/BTC 47.07%` -> 4. `SALT/BTC 30.24%` -> 5. `STORJ/BTC 27.24%` +> Performance: +> 1. `RCN/BTC 0.003 BTC (57.77%) (1)` +> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)` +> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)` +> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)` +> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)` > ... ### /balance diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 168938973..edc0a1404 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -1,3 +1,5 @@ +# Windows installation + We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure). If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. @@ -21,7 +23,7 @@ git clone https://github.com/freqtrade/freqtrade.git Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) +As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.20‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version). Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows. Other versions must be downloaded from the above link. diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 268e3eeef..e072e12cb 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -7,6 +7,7 @@ from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file from freqtrade.exceptions import OperationalException +from freqtrade.optimize.optimize_reports import show_backtest_result from freqtrade.state import RunMode @@ -125,6 +126,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] + + metrics = val['results_metrics'] + if 'strategy_name' in metrics: + show_backtest_result(metrics['strategy_name'], metrics, + metrics['stake_currency']) + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, header_str="Epoch details") @@ -132,11 +139,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: """ Filter our items from the list of hyperopt results + TODO: after 2021.5 remove all "legacy" mode queries. """ if filteroptions['only_best']: epochs = [x for x in epochs if x['is_best']] if filteroptions['only_profitable']: - epochs = [x for x in epochs if x['results_metrics']['profit'] > 0] + epochs = [x for x in epochs if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0)) > 0] epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) @@ -153,34 +162,55 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: return epochs +def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): + """ + Filter epochs with trade-counts > trades + """ + return [ + x for x in epochs + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades', 0) + ) > trade_count + ] + + def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_trades'] > 0: - epochs = [ - x for x in epochs - if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] - ] + epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) + if filteroptions['filter_max_trades'] > 0: epochs = [ x for x in epochs - if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades') + ) < filteroptions['filter_max_trades'] ] return epochs def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: + def get_duration_value(x): + # Duration in minutes ... + if 'duration' in x['results_metrics']: + return x['results_metrics']['duration'] + else: + # New mode + avg = x['results_metrics']['holding_avg'] + return avg.total_seconds() // 60 + if filteroptions['filter_min_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time'] + if get_duration_value(x) > filteroptions['filter_min_avg_time'] ] if filteroptions['filter_max_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] + if get_duration_value(x) < filteroptions['filter_max_avg_time'] ] return epochs @@ -189,28 +219,36 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] > filteroptions['filter_min_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 + ) > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] < filteroptions['filter_max_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 + ) < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total_abs', 0) + ) > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total_abs', 0) + ) < filteroptions['filter_max_total_profit'] ] return epochs @@ -218,11 +256,11 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] if filteroptions['filter_max_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 6072e296c..6979c8cd1 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -3,6 +3,7 @@ This module contains the argument manager class """ import logging import re +from datetime import datetime from typing import Optional import arrow @@ -43,7 +44,7 @@ class TimeRange: self.startts = self.startts - seconds def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, - min_date: arrow.Arrow) -> None: + min_date: datetime) -> None: """ Adjust startts by candles. Applies only if no startup-candles have been available. @@ -54,11 +55,11 @@ class TimeRange: :return: None (Modifies the object in place) """ if (not self.starttype or (startup_candles - and min_date.int_timestamp >= self.startts)): + 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.int_timestamp + timeframe_secs * startup_candles) + self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles) self.starttype = 'date' @staticmethod diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bfd1e72f1..e42b9d4b8 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +TIMEOUT_UNITS = ['minutes', 'seconds'] DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -137,7 +138,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'buy': {'type': 'number', 'minimum': 1}, - 'sell': {'type': 'number', 'minimum': 1} + 'sell': {'type': 'number', 'minimum': 1}, + 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } }, 'bid_strategy': { @@ -258,7 +260,13 @@ CONF_SCHEMA = { 'enum': TELEGRAM_SETTING_OPTIONS, 'default': 'off' }, - 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell': { + 'type': ['string', 'object'], + 'additionalProperties': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS + } + }, 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'sell_fill': { 'type': 'string', diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e07b0a40f..e7af5eab8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -156,33 +156,35 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non data = data['strategy'][strategy]['trades'] df = pd.DataFrame(data) - df['open_date'] = pd.to_datetime(df['open_date'], - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - utc=True, - infer_datetime_format=True - ) + if not df.empty: + df['open_date'] = pd.to_datetime(df['open_date'], + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + utc=True, + infer_datetime_format=True + ) else: # old format - only with lists. df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD) - - df['open_date'] = pd.to_datetime(df['open_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - # Create compatibility with new format - df['profit_abs'] = df['close_rate'] - df['open_rate'] - if 'profit_ratio' not in df.columns: - df['profit_ratio'] = df['profit_percent'] - df = df.sort_values("open_date").reset_index(drop=True) + if not df.empty: + df['open_date'] = pd.to_datetime(df['open_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + # Create compatibility with new format + df['profit_abs'] = df['close_rate'] - df['open_rate'] + if not df.empty: + if 'profit_ratio' not in df.columns: + df['profit_ratio'] = df['profit_percent'] + df = df.sort_values("open_date").reset_index(drop=True) return df diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index af6c6a2ef..ffee0c52c 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -145,6 +145,27 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', return df +def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange, + startup_candles: int) -> Dict[str, DataFrame]: + """ + Trim startup period from analyzed dataframes + :param preprocessed: Dict of pair: dataframe + :param timerange: timerange (use start and end date if available) + :param startup_candles: Startup-candles that should be removed + :return: Dict of trimmed dataframes + """ + processed: Dict[str, DataFrame] = {} + + for pair, df in preprocessed.items(): + trimed_df = trim_dataframe(df, timerange, startup_candles=startup_candles) + if not trimed_df.empty: + processed[pair] = trimed_df + else: + logger.warning(f'{pair} has no data left after adjusting for startup candles, ' + f'skipping.') + return processed + + def order_book_to_dataframe(bids: list, asks: list) -> DataFrame: """ TODO: This should get a dedicated test diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b4dea0743..1a86eece5 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -19,14 +19,25 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) +NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' +MAX_DATAFRAME_CANDLES = 1000 + class DataProvider: - def __init__(self, config: dict, exchange: Exchange, pairlists=None) -> None: + def __init__(self, config: dict, exchange: Optional[Exchange], pairlists=None) -> None: self._config = config self._exchange = exchange self._pairlists = pairlists self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} + self.__slice_index: Optional[int] = None + + def _set_dataframe_max_index(self, limit_index: int): + """ + Limit analyzed dataframe to max specified index. + :param limit_index: dataframe index. + """ + self.__slice_index = limit_index def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -45,40 +56,6 @@ class DataProvider: """ self._pairlists = pairlists - def refresh(self, - pairlist: ListPairsWithTimeframes, - helping_pairs: ListPairsWithTimeframes = None) -> None: - """ - Refresh data, called with each cycle - """ - if helping_pairs: - self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) - else: - self._exchange.refresh_latest_ohlcv(pairlist) - - @property - def available_pairs(self) -> ListPairsWithTimeframes: - """ - 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, timeframe: str = None, copy: bool = True) -> DataFrame: - """ - Get candle (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 timeframe: 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, timeframe or self._config['timeframe']), - copy=copy) - else: - return DataFrame() - def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame: """ Get stored historical candle (OHLCV) data @@ -111,47 +88,27 @@ class DataProvider: def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]: """ + Retrieve the analyzed dataframe. Returns the full dataframe in trade mode (live / dry), + and the last 1000 candles (up to the time evaluated at this moment) in all other modes. :param pair: pair to get the data for :param timeframe: timeframe to get data for :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe combination. Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached. """ - if (pair, timeframe) in self.__cached_pairs: - return self.__cached_pairs[(pair, timeframe)] + pair_key = (pair, timeframe) + if pair_key in self.__cached_pairs: + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + df, date = self.__cached_pairs[pair_key] + else: + df, date = self.__cached_pairs[pair_key] + if self.__slice_index is not None: + max_index = self.__slice_index + df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index] + return df, date else: - return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) - def market(self, pair: str) -> Optional[Dict[str, Any]]: - """ - Return market data for the pair - :param pair: Pair to get the data for - :return: Market data dict from ccxt or None if market info is not available for the pair - """ - return self._exchange.markets.get(pair) - - def ticker(self, pair: str): - """ - Return last ticker data from exchange - :param pair: Pair to get the data for - :return: Ticker dict from exchange or empty dict if ticker is not available for the pair - """ - try: - return self._exchange.fetch_ticker(pair) - except ExchangeError: - return {} - - def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: - """ - Fetch latest l2 orderbook data - Warning: Does a network request - so use with common sense. - :param pair: pair to get the data for - :param maximum: Maximum number of orderbook entries to query - :return: dict including bids/asks with a total of `maximum` entries. - """ - return self._exchange.fetch_l2_order_book(pair, maximum) - @property def runmode(self) -> RunMode: """ @@ -173,3 +130,86 @@ class DataProvider: return self._pairlists.whitelist.copy() else: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") + + def clear_cache(self): + """ + Clear pair dataframe cache. + """ + self.__cached_pairs = {} + + # Exchange functions + + def refresh(self, + pairlist: ListPairsWithTimeframes, + helping_pairs: ListPairsWithTimeframes = None) -> None: + """ + Refresh data, called with each cycle + """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + if helping_pairs: + self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) + else: + self._exchange.refresh_latest_ohlcv(pairlist) + + @property + def available_pairs(self) -> ListPairsWithTimeframes: + """ + Return a list of tuples containing (pair, timeframe) for which data is currently cached. + Should be whitelist + open trades. + """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + return list(self._exchange._klines.keys()) + + def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: + """ + Get candle (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 timeframe: 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._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + return self._exchange.klines((pair, timeframe or self._config['timeframe']), + copy=copy) + else: + return DataFrame() + + def market(self, pair: str) -> Optional[Dict[str, Any]]: + """ + Return market data for the pair + :param pair: Pair to get the data for + :return: Market data dict from ccxt or None if market info is not available for the pair + """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + return self._exchange.markets.get(pair) + + def ticker(self, pair: str): + """ + Return last ticker data from exchange + :param pair: Pair to get the data for + :return: Ticker dict from exchange or empty dict if ticker is not available for the pair + """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + try: + return self._exchange.fetch_ticker(pair) + except ExchangeError: + return {} + + def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: + """ + Fetch latest l2 orderbook data + Warning: Does a network request - so use with common sense. + :param pair: pair to get the data for + :param maximum: Maximum number of orderbook entries to query + :return: dict including bids/asks with a total of `maximum` entries. + """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + return self._exchange.fetch_l2_order_book(pair, maximum) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 58965abe0..86e9f75e6 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -265,9 +265,13 @@ def _download_trades_history(exchange: Exchange, """ try: - since = timerange.startts * 1000 if \ - (timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift( - days=-new_pairs_days).float_timestamp) * 1000 + until = None + if (timerange and timerange.starttype == 'date'): + since = timerange.startts * 1000 + if timerange.stoptype == 'date': + until = timerange.stopts * 1000 + else: + since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000 trades = data_handler.trades_load(pair) @@ -295,6 +299,7 @@ def _download_trades_history(exchange: Exchange, # Default since_ms to 30 days if nothing is given new_trades = exchange.get_historic_trades(pair=pair, since=since, + until=until, from_id=from_id, ) trades.extend(new_trades[1]) @@ -367,7 +372,7 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], logger.exception(f'Could not convert {pair} to OHLCV.') -def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]: """ Get the maximum common timerange for the given backtest data. @@ -375,7 +380,7 @@ def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] :return: tuple containing min_date, max_date """ timeranges = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + (frame['date'].min().to_pydatetime(), frame['date'].max().to_pydatetime()) for frame in data.values() ] return (min(timeranges, key=operator.itemgetter(0))[0], diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 334aabfab..4bc0d660b 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -1,6 +1,8 @@ # pragma pylint: disable=W0603 """ Edge positioning package """ import logging +from collections import defaultdict +from copy import deepcopy from typing import Any, Dict, List, NamedTuple import arrow @@ -12,8 +14,10 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.exceptions import OperationalException +from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist -from freqtrade.strategy.interface import SellType +from freqtrade.state import RunMode +from freqtrade.strategy.interface import IStrategy, SellType logger = logging.getLogger(__name__) @@ -45,7 +49,7 @@ class Edge: self.config = config self.exchange = exchange - self.strategy = strategy + self.strategy: IStrategy = strategy self.edge_config = self.config.get('edge', {}) self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs @@ -102,14 +106,33 @@ class Edge: logger.info('Using local backtesting data (using whitelist in given config) ...') if self._refresh_pairs: + timerange_startup = deepcopy(self._timerange) + timerange_startup.subtract_start(timeframe_to_seconds( + self.strategy.timeframe) * self.strategy.startup_candle_count) refresh_data( datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, timeframe=self.strategy.timeframe, - timerange=self._timerange, + timerange=timerange_startup, data_format=self.config.get('dataformat_ohlcv', 'json'), ) + # Download informative pairs too + res = defaultdict(list) + for p, t in self.strategy.informative_pairs(): + res[t].append(p) + for timeframe, inf_pairs in res.items(): + timerange_startup = deepcopy(self._timerange) + timerange_startup.subtract_start(timeframe_to_seconds( + timeframe) * self.strategy.startup_candle_count) + refresh_data( + datadir=self.config['datadir'], + pairs=inf_pairs, + exchange=self.exchange, + timeframe=timeframe, + timerange=timerange_startup, + data_format=self.config.get('dataformat_ohlcv', 'json'), + ) data = load_data( datadir=self.config['datadir'], @@ -125,8 +148,11 @@ class Edge: self._cached_pairs = {} logger.critical("No data found. Edge is stopped ...") return False - + # Fake run-mode to Edge + prior_rm = self.config['runmode'] + self.config['runmode'] = RunMode.EDGE preprocessed = self.strategy.ohlcvdata_to_dataframe(data) + self.config['runmode'] = prior_rm # Print timeframe min_date, max_date = get_timerange(preprocessed) @@ -183,7 +209,7 @@ class Edge: if pair in self._cached_pairs: return self._cached_pairs[pair].stoploss else: - logger.warning('tried to access stoploss of a non-existing pair, ' + logger.warning(f'Tried to access stoploss of non-existing pair {pair}, ' 'strategy stoploss is returned instead.') return self.strategy.stoploss @@ -214,7 +240,7 @@ class Edge: return self._final_pairs - def accepted_pairs(self) -> list: + def accepted_pairs(self) -> List[Dict[str, Any]]: """ return a list of accepted pairs along with their winrate, expectancy and stoploss """ diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 889bb49c2..015e0c869 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -7,6 +7,7 @@ from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit +from freqtrade.exchange.coinbasepro import Coinbasepro from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, @@ -14,5 +15,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, timeframe_to_seconds, validate_exchange, validate_exchanges) from freqtrade.exchange.ftx import Ftx +from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 4a44bb42d..163f8c44e 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -18,7 +18,6 @@ class Bybit(Exchange): may still not work as expected. """ - # fetchCurrencies API point requires authentication for Bybit, _ft_has: Dict = { "ohlcv_candle_limit": 200, } diff --git a/freqtrade/exchange/coinbasepro.py b/freqtrade/exchange/coinbasepro.py new file mode 100644 index 000000000..7dd9c80dc --- /dev/null +++ b/freqtrade/exchange/coinbasepro.py @@ -0,0 +1,23 @@ +""" CoinbasePro exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Coinbasepro(Exchange): + """ + CoinbasePro 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. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 300, + } diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f61f89fcb..93d8f7584 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -59,6 +59,7 @@ class Exchange: _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], + "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_partial_candle": True, "trades_pagination": "time", # Possible are "time" or "id" @@ -465,7 +466,7 @@ class Exchange: def amount_to_precision(self, pair: str, amount: float) -> float: ''' Returns the amount to buy or sell to a precision the Exchange accepts - Reimplementation of ccxt internal methods - ensuring we can test the result is correct + Re-implementation of ccxt internal methods - ensuring we can test the result is correct based on our definitions. ''' if self.markets[pair]['precision']['amount']: @@ -479,7 +480,7 @@ class Exchange: def price_to_precision(self, pair: str, price: float) -> float: ''' Returns the price rounded up to the precision the Exchange accepts. - Partial Reimplementation of ccxt internal method decimal_to_precision(), + Partial Re-implementation of ccxt internal method decimal_to_precision(), which does not support rounding up TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and align with amount_to_precision(). @@ -862,10 +863,11 @@ class Exchange: "Fetching pair %s, interval %s, since %s %s...", pair, timeframe, since_ms, s ) - + params = self._ft_has.get('ohlcv_params', {}) data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, - limit=self.ohlcv_candle_limit(timeframe)) + limit=self.ohlcv_candle_limit(timeframe), + params=params) # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) @@ -1118,6 +1120,27 @@ class Exchange: return order + def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict: + """ + Cancel stoploss order returning a result. + Creates a fake result if cancel order returns a non-usable result + and fetch_order does not work (certain exchanges don't return cancelled orders) + :param order_id: stoploss-order-id to cancel + :param pair: Pair corresponding to order_id + :param amount: Amount to use for fake response + :return: Result from either cancel_order if usable, or fetch_order + """ + corder = self.cancel_stoploss_order(order_id, pair) + if self.is_cancel_order_result_suitable(corder): + return corder + try: + order = self.fetch_stoploss_order(order_id, pair) + except InvalidOrderException: + logger.warning(f"Could not fetch cancelled stoploss order {order_id}.") + order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} + + return order + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: @@ -1237,6 +1260,9 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + return order['id'] + @retrier def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, price: float = 1, taker_or_maker: str = 'maker') -> float: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index fdfd5a674..9009e9492 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier +from freqtrade.misc import safe_value_fallback2 logger = logging.getLogger(__name__) @@ -135,3 +136,8 @@ class Ftx(Exchange): f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + if order['type'] == 'stop': + return safe_value_fallback2(order['info'], order, 'orderId', 'id') + return order['id'] diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py new file mode 100644 index 000000000..a48c9a198 --- /dev/null +++ b/freqtrade/exchange/hitbtc.py @@ -0,0 +1,23 @@ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Hitbtc(Exchange): + """ + Hitbtc 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. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1000, + "ohlcv_params": {"sort": "DESC"} + } diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 786f1b592..6f1fa409a 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -53,6 +53,8 @@ class Kraken(Exchange): # x["side"], x["amount"], ) for x in orders] for bal in balances: + if not isinstance(balances[bal], dict): + continue balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal) balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c2b15d23f..56596f2b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,7 +11,6 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from pandas import DataFrame from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -268,7 +267,7 @@ class FreqtradeBot(LoggingMixin): def update_closed_trades_without_assigned_fees(self): """ Update closed trades without close fees assigned. - Only acts when Orders are in the database, otherwise the last orderid is unknown. + Only acts when Orders are in the database, otherwise the last order-id is unknown. """ if self.config['dry_run']: # Updating open orders in dry-run does not make sense and will fail. @@ -553,7 +552,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, - time_in_force=time_in_force): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -602,6 +601,7 @@ class FreqtradeBot(LoggingMixin): pair=pair, stake_amount=stake_amount, amount=amount, + is_open=True, amount_requested=amount_requested, fee_open=fee, fee_close=fee, @@ -631,7 +631,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy(self, trade: Trade, order_type: str) -> None: """ - Sends rpc notification when a buy occured. + Sends rpc notification when a buy occurred. """ msg = { 'trade_id': trade.id, @@ -653,7 +653,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ - Sends rpc notification when a buy cancel occured. + Sends rpc notification when a buy cancel occurred. """ current_rate = self.get_buy_rate(trade.pair, False) @@ -714,7 +714,7 @@ class FreqtradeBot(LoggingMixin): except DependencyException as exception: logger.warning('Unable to sell trade %s: %s', trade.pair, exception) - # Updating wallets if any trade occured + # Updating wallets if any trade occurred if trades_closed: self.wallets.update() @@ -784,10 +784,10 @@ class FreqtradeBot(LoggingMixin): config_ask_strategy = self.config.get('ask_strategy', {}) - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) @@ -814,13 +814,13 @@ class FreqtradeBot(LoggingMixin): # resulting in outdated RPC messages self._sell_rate_cache[trade.pair] = sell_rate - if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -933,14 +933,15 @@ class FreqtradeBot(LoggingMixin): :return: None """ if self.exchange.stoploss_adjust(trade.stop_loss, order): - # we check if the update is neccesary + # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: # cancelling the current stoploss on exchange first logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " f"(orderid:{order['id']}) in order to add another one ...") try: - co = self.exchange.cancel_stoploss_order(order['id'], trade.pair) + co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair, + trade.amount) trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {order['id']} " @@ -951,13 +952,13 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float, + def _check_and_execute_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: """ Check and execute sell """ should_sell = self.strategy.should_sell( - dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) @@ -974,15 +975,16 @@ class FreqtradeBot(LoggingMixin): 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 - + timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') + timeout_kwargs = {timeout_unit: -timeout} + timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).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 + Check if any orders are timed out and cancel if necessary :param timeoutvalue: Number of minutes until order is considered timed out :return: None """ @@ -1044,6 +1046,16 @@ class FreqtradeBot(LoggingMixin): # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('cancelled', 'canceled', 'closed'): + filled_val = order.get('filled', 0.0) or 0.0 + filled_stake = filled_val * trade.open_rate + minstake = self.exchange.get_min_pair_stake_amount( + trade.pair, trade.open_rate, self.strategy.stoploss) + + if filled_val > 0 and filled_stake < minstake: + logger.warning( + f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"as the filled amount of {filled_val} would result in an unsellable trade.") + return False corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. @@ -1173,7 +1185,9 @@ class FreqtradeBot(LoggingMixin): # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: try: - self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair) + co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, + trade.pair, trade.amount) + trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") @@ -1191,8 +1205,8 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, - sell_reason=sell_reason.sell_reason): + time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, + current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of selling {trade.pair}") return False @@ -1221,7 +1235,7 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, trade.open_order_id, order) Trade.query.session.flush() - # Lock pair for one candle to prevent immediate rebuys + # Lock pair for one candle to prevent immediate re-buys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') @@ -1231,7 +1245,7 @@ class FreqtradeBot(LoggingMixin): def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ - Sends rpc notification when a sell occured. + Sends rpc notification when a sell occurred. """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) @@ -1272,7 +1286,7 @@ class FreqtradeBot(LoggingMixin): def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ - Sends rpc notification when a sell cancel occured. + Sends rpc notification when a sell cancel occurred. """ if trade.sell_order_status == reason: return @@ -1325,7 +1339,7 @@ class FreqtradeBot(LoggingMixin): Handles closing both buy and sell orders. :param trade: Trade object of the trade we're analyzing :param order_id: Order-id of the order we're analyzing - :param action_order: Already aquired order object + :param action_order: Already acquired order object :return: True if order has been cancelled without being filled partially, False otherwise """ if not order_id: @@ -1395,7 +1409,7 @@ class FreqtradeBot(LoggingMixin): def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Detect and update trade fee. - Calls trade.update_fee() uppon correct detection. + Calls trade.update_fee() upon correct detection. Returns modified amount if the fee was taken from the destination currency. Necessary for exchanges which charge fees in base currency (e.g. binance) :return: identical (or new) amount for the trade @@ -1428,8 +1442,8 @@ class FreqtradeBot(LoggingMixin): """ fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. """ - trades = self.exchange.get_trades_for_order(order['id'], trade.pair, - trade.open_date) + trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), + trade.pair, trade.open_date) if len(trades) == 0: logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 6508363d6..2e255901e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -6,7 +6,7 @@ import logging import re from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Iterator, List from typing.io import IO import rapidjson @@ -202,3 +202,14 @@ def render_template_with_fallback(templatefile: str, templatefallbackfile: str, return render_template(templatefile, arguments) except TemplateNotFound: return render_template(templatefallbackfile, arguments) + + +def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]: + """ + Split lst into chunks of the size n. + :param lst: list to split into chunks + :param n: number of max elements per chunk + :return: None + """ + for chunk in range(0, len(lst), n): + yield (lst[chunk:chunk + n]) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7b62661d3..cbc0995aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -15,7 +15,7 @@ from freqtrade.configuration import TimeRange, remove_credentials, validate_conf from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe -from freqtrade.data.converter import trim_dataframe +from freqtrade.data.converter import trim_dataframes from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -63,9 +63,7 @@ class Backtesting: self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - - dataprovider = DataProvider(self.config, self.exchange) - IStrategy.dp = dataprovider + self.dataprovider = DataProvider(self.config, None) if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): @@ -96,7 +94,7 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) - dataprovider.add_pairlisthandler(self.pairlists) + self.dataprovider.add_pairlisthandler(self.pairlists) self.pairlists.refresh_pairlist() if len(self.pairlists.whitelist) == 0: @@ -112,15 +110,11 @@ class Backtesting: PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False PairLocks.reset_locks() - if self.config.get('enable_protections', False): - self.protections = ProtectionManager(self.config) self.wallets = Wallets(self.config, self.exchange, log=False) # 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]) def __del__(self): LoggingMixin.show_output = True @@ -132,10 +126,17 @@ class Backtesting: Load strategy into backtesting """ self.strategy: IStrategy = strategy + strategy.dp = self.dataprovider # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + if self.config.get('enable_protections', False): + conf = self.config + if hasattr(strategy, 'protections'): + conf = deepcopy(conf) + conf['protections'] = strategy.protections + self.protections = ProtectionManager(conf) def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: """ @@ -159,7 +160,7 @@ class Backtesting: logger.info(f'Loading data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + f'({(max_date - min_date).days} days).') # Adjust startts forward if not enough data is available timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), @@ -176,6 +177,8 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + self.rejected_trades = 0 + self.dataprovider.clear_cache() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ @@ -189,8 +192,9 @@ class Backtesting: data: Dict = {} # Create dict with data for pair, pair_data in processed.items(): - pair_data.loc[:, 'buy'] = 0 # cleanup from previous run - pair_data.loc[:, 'sell'] = 0 # cleanup from previous run + if not pair_data.empty: + pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist + pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() @@ -214,6 +218,12 @@ class Backtesting: """ # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if trade.stop_loss > sell_row[HIGH_IDX]: + # our stoploss was already higher than candle high, + # possibly due to a cancelled trade exit. + # sell at open price. + return sell_row[OPEN_IDX] + # Set close_rate to stoploss return trade.stop_loss elif sell.sell_type == (SellType.ROI): @@ -247,10 +257,9 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade, - sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) @@ -267,7 +276,8 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_reason): + sell_reason=sell.sell_reason, + current_time=sell_row[DATE_IDX].to_pydatetime()): return None trade.close(closerate, show_msg=False) @@ -287,7 +297,7 @@ class Backtesting: # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], - time_in_force=time_in_force): + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): @@ -327,10 +337,18 @@ class Backtesting: trades.append(trade1) return trades + def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: + # Always allow trades when max_open_trades is enabled. + if max_open_trades <= 0 or open_trade_count < max_open_trades: + return True + # Rejected trade + self.rejected_trades += 1 + return False + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, - enable_protections: bool = False) -> DataFrame: + enable_protections: bool = False) -> Dict[str, Any]: """ Implement backtesting functionality @@ -349,6 +367,10 @@ class Backtesting: trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) + # Update dataprovider cache + for pair, dataframe in processed.items(): + self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) + # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) data: Dict = self._get_ohlcv_as_lists(processed) @@ -365,8 +387,9 @@ class Backtesting: open_trade_count_start = open_trade_count for i, pair in enumerate(data): + row_index = indexes[pair] try: - row = data[pair][indexes[pair]] + row = data[pair][row_index] except IndexError: # missing Data for one pair at the end. # Warnings for this are shown during data loading @@ -375,16 +398,22 @@ class Backtesting: # Waits until the time-counter reaches the start of the data for this pair. if row[DATE_IDX] > tmp: continue - indexes[pair] += 1 + + row_index += 1 + self.dataprovider._set_dataframe_max_index(row_index) + indexes[pair] = row_index # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row - if ((position_stacking or len(open_trades[pair]) == 0) - and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) - and tmp != end_date - and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 - and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + if ( + (position_stacking or len(open_trades[pair]) == 0) + and self.trade_slot_available(max_open_trades, open_trade_count_start) + and tmp != end_date + and row[BUY_IDX] == 1 + and row[SELL_IDX] != 1 + and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) + ): trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades @@ -398,7 +427,7 @@ class Backtesting: for trade in open_trades[pair]: # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(processed[pair], trade, row) + trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: # logger.debug(f"{pair} - Backtesting sell {trade}") @@ -417,7 +446,14 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) self.wallets.update() - return trade_list_to_dataframe(trades) + results = trade_list_to_dataframe(trades) + return { + 'results': results, + 'config': self.strategy.config, + 'locks': PairLocks.get_all_locks(), + 'rejected_signals': self.rejected_trades, + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) @@ -439,32 +475,32 @@ class Backtesting: preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe - for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange, - startup_candles=self.required_startup) - min_date, max_date = history.get_timerange(preprocessed) + preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup) + if not preprocessed: + raise OperationalException( + "No data left after adjusting for startup candles.") + + min_date, max_date = history.get_timerange(preprocessed) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + f'({(max_date - min_date).days} days).') # Execute backtest and store results results = self.backtest( processed=preprocessed, - start_date=min_date.datetime, - end_date=max_date.datetime, + start_date=min_date, + end_date=max_date, max_open_trades=max_open_trades, position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - self.all_results[self.strategy.get_strategy_name()] = { - 'results': results, - 'config': self.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } + }) + self.all_results[self.strategy.get_strategy_name()] = results + return min_date, max_date def start(self) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9af3710a3..49273d3de 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,20 +7,21 @@ This module contains the hyperopt logic import logging import random import warnings -from datetime import datetime +from datetime import datetime, timezone from math import ceil -from operator import itemgetter from pathlib import Path from typing import Any, Dict, List, Optional +import numpy as np import progressbar +import rapidjson from colorama import Fore, Style 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 freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN -from freqtrade.data.converter import trim_dataframe +from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting @@ -29,8 +30,8 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver -from freqtrade.strategy import IStrategy # Suppress scikit-learn FutureWarnings from skopt @@ -64,6 +65,13 @@ class Hyperopt: custom_hyperopt: IHyperOpt def __init__(self, config: Dict[str, Any]) -> None: + self.buy_space: List[Dimension] = [] + self.sell_space: List[Dimension] = [] + self.roi_space: List[Dimension] = [] + self.stoploss_space: List[Dimension] = [] + self.trailing_space: List[Dimension] = [] + self.dimensions: List[Dimension] = [] + self.config = config self.backtesting = Backtesting(self.config) @@ -72,15 +80,15 @@ class Hyperopt: self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") strategy = str(self.config['strategy']) - self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / - f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') + self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' / + f'strategy_{strategy}_{time_now}.fthypt') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) @@ -90,9 +98,7 @@ class Hyperopt: self.clean_hyperopt() self.num_epochs_saved = 0 - - # Previous evaluations - self.epochs: List = [] + self.current_best_epoch: Optional[Dict[str, Any]] = None # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_indicators'): @@ -113,7 +119,7 @@ class Hyperopt: self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False) - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): # Make sure use_sell_signal is enabled if 'ask_strategy' not in self.config: self.config['ask_strategy'] = {} @@ -139,9 +145,7 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() - def _get_params_dict(self, raw_params: List[Any]) -> Dict: - - dimensions: List[Dimension] = self.dimensions + def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: # Ensure the number of dimensions match # the number of parameters in the list. @@ -152,21 +156,30 @@ class Hyperopt: # and the values are taken from the list of parameters. return {d.name: v for d, v in zip(dimensions, raw_params)} - def _save_results(self) -> None: + def _save_result(self, epoch: Dict) -> None: """ Save hyperopt results to file + Store one line per epoch. + While not a valid json object - this allows appending easily. + :param epoch: result dictionary for this epoch. """ - num_epochs = len(self.epochs) - if num_epochs > self.num_epochs_saved: - logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.") - dump(self.epochs, self.results_file) - self.num_epochs_saved = num_epochs - logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " - f"saved to '{self.results_file}'.") - # Store hyperopt filename - latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN) - file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, - log=False) + def default_parser(x): + if isinstance(x, np.integer): + return int(x) + return str(x) + + with self.results_file.open('a') as f: + rapidjson.dump(epoch, f, default=default_parser, + number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) + f.write("\n") + + self.num_epochs_saved += 1 + logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " + f"saved to '{self.results_file}'.") + # Store hyperopt filename + latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN) + file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, + log=False) def _get_params_details(self, params: Dict) -> Dict: """ @@ -174,18 +187,16 @@ class Hyperopt: """ result: Dict = {} - if self.has_space('buy'): - result['buy'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('buy')} - if self.has_space('sell'): - result['sell'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('sell')} - if self.has_space('roi'): - result['roi'] = self.custom_hyperopt.generate_roi_table(params) - if self.has_space('stoploss'): - result['stoploss'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('stoploss')} - if self.has_space('trailing'): + if HyperoptTools.has_space(self.config, 'buy'): + result['buy'] = {p.name: params.get(p.name) for p in self.buy_space} + if HyperoptTools.has_space(self.config, 'sell'): + result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} + if HyperoptTools.has_space(self.config, 'roi'): + result['roi'] = {str(k): v for k, v in + self.custom_hyperopt.generate_roi_table(params).items()} + if HyperoptTools.has_space(self.config, 'stoploss'): + result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} + if HyperoptTools.has_space(self.config, 'trailing'): result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) return result @@ -207,71 +218,58 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - def has_space(self, space: str) -> bool: + def init_spaces(self): """ - Tell if the space value is contained in the configuration + Assign the dimensions in the hyperoptimization space. """ - # The 'trailing' space is not included in the 'default' set of spaces - if space == 'trailing': - return any(s in self.config['spaces'] for s in [space, 'all']) - else: - return any(s in self.config['spaces'] for s in [space, 'all', 'default']) - def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: - """ - Return the dimensions in the hyperoptimization space. - :param space: Defines hyperspace to return dimensions for. - If None, then the self.has_space() will be used to return dimensions - for all hyperspaces used. - """ - spaces: List[Dimension] = [] - - if space == 'buy' or (space is None and self.has_space('buy')): + if HyperoptTools.has_space(self.config, 'buy'): logger.debug("Hyperopt has 'buy' space") - spaces += self.custom_hyperopt.indicator_space() + self.buy_space = self.custom_hyperopt.indicator_space() - if space == 'sell' or (space is None and self.has_space('sell')): + if HyperoptTools.has_space(self.config, 'sell'): logger.debug("Hyperopt has 'sell' space") - spaces += self.custom_hyperopt.sell_indicator_space() + self.sell_space = self.custom_hyperopt.sell_indicator_space() - if space == 'roi' or (space is None and self.has_space('roi')): + if HyperoptTools.has_space(self.config, 'roi'): logger.debug("Hyperopt has 'roi' space") - spaces += self.custom_hyperopt.roi_space() + self.roi_space = self.custom_hyperopt.roi_space() - if space == 'stoploss' or (space is None and self.has_space('stoploss')): + if HyperoptTools.has_space(self.config, 'stoploss'): logger.debug("Hyperopt has 'stoploss' space") - spaces += self.custom_hyperopt.stoploss_space() + self.stoploss_space = self.custom_hyperopt.stoploss_space() - if space == 'trailing' or (space is None and self.has_space('trailing')): + if HyperoptTools.has_space(self.config, 'trailing'): logger.debug("Hyperopt has 'trailing' space") - spaces += self.custom_hyperopt.trailing_space() - - return spaces + self.trailing_space = self.custom_hyperopt.trailing_space() + self.dimensions = (self.buy_space + self.sell_space + self.roi_space + + self.stoploss_space + self.trailing_space) def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: """ Used Optimize function. Called once per epoch to optimize whatever is configured. Keep this function as optimized as possible! """ - params_dict = self._get_params_dict(raw_params) - params_details = self._get_params_details(params_dict) + backtest_start_time = datetime.now(timezone.utc) + params_dict = self._get_params_dict(self.dimensions, raw_params) - if self.has_space('roi'): + # Apply parameters + if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) - if self.has_space('buy'): + if HyperoptTools.has_space(self.config, 'buy'): self.backtesting.strategy.advise_buy = ( # type: ignore self.custom_hyperopt.buy_strategy_generator(params_dict)) - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): self.backtesting.strategy.advise_sell = ( # type: ignore self.custom_hyperopt.sell_strategy_generator(params_dict)) - if self.has_space('stoploss'): + if HyperoptTools.has_space(self.config, 'stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] - if self.has_space('trailing'): + if HyperoptTools.has_space(self.config, 'trailing'): d = self.custom_hyperopt.generate_trailing_params(params_dict) self.backtesting.strategy.trailing_stop = d['trailing_stop'] self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive'] @@ -280,30 +278,42 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] - processed = load(self.data_pickle_file) - - min_date, max_date = get_timerange(processed) - - backtesting_results = self.backtesting.backtest( + with self.data_pickle_file.open('rb') as f: + processed = load(f, mmap_mode='r') + bt_results = self.backtesting.backtest( processed=processed, - start_date=min_date.datetime, - end_date=max_date.datetime, + start_date=self.min_date, + end_date=self.max_date, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, enable_protections=self.config.get('enable_protections', False), - ) - return self._get_results_dict(backtesting_results, min_date, max_date, - params_dict, params_details, + backtest_end_time = datetime.now(timezone.utc) + bt_results.update({ + 'backtest_start_time': int(backtest_start_time.timestamp()), + 'backtest_end_time': int(backtest_end_time.timestamp()), + }) + + return self._get_results_dict(bt_results, self.min_date, self.max_date, + params_dict, processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict[str, DataFrame]): - results_metrics = self._calculate_results_metrics(backtesting_results) - results_explanation = self._format_results_explanation_string(results_metrics) + params_dict, processed: Dict[str, DataFrame] + ) -> Dict[str, Any]: + params_details = self._get_params_details(params_dict) - trade_count = results_metrics['trade_count'] - total_profit = results_metrics['total_profit'] + strat_stats = generate_strategy_stats( + processed, self.backtesting.strategy.get_strategy_name(), + backtesting_results, min_date, max_date, market_change=0 + ) + results_explanation = HyperoptTools.format_results_explanation_string( + strat_stats, self.config['stake_currency']) + + not_optimized = self.backtesting.strategy.get_params_dict() + + trade_count = strat_stats['total_trades'] + total_profit = strat_stats['profit_total'] # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -311,50 +321,20 @@ class Hyperopt: # path. We do not want to optimize 'hodl' strategies. loss: float = MAX_LOSS if trade_count >= self.config['hyperopt_min_trades']: - loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, - min_date=min_date.datetime, max_date=max_date.datetime, + loss = self.calculate_loss(results=backtesting_results['results'], + trade_count=trade_count, + min_date=min_date, max_date=max_date, config=self.config, processed=processed) return { 'loss': loss, 'params_dict': params_dict, 'params_details': params_details, - 'results_metrics': results_metrics, + 'params_not_optimized': not_optimized, + 'results_metrics': strat_stats, 'results_explanation': results_explanation, 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: - wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0]) - draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0]) - losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0]) - return { - 'trade_count': len(backtesting_results.index), - 'wins': wins, - 'draws': draws, - 'losses': losses, - 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", - 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0, - 'median_profit': backtesting_results['profit_ratio'].median() * 100.0, - 'total_profit': backtesting_results['profit_abs'].sum(), - 'profit': backtesting_results['profit_ratio'].sum() * 100.0, - 'duration': backtesting_results['trade_duration'].mean(), - } - - def _format_results_explanation_string(self, results_metrics: Dict) -> str: - """ - Return the formatted results explanation in a string - """ - stake_cur = self.config['stake_currency'] - return (f"{results_metrics['trade_count']:6d} trades. " - f"{results_metrics['wins']}/{results_metrics['draws']}" - f"/{results_metrics['losses']} Wins/Draws/Losses. " - f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " - f"Median profit {results_metrics['median_profit']: 6.2f}%. " - f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " - f"({results_metrics['profit']: 7.2f}%). " - f"Avg duration {results_metrics['duration']:5.1f} min." - ) - def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( dimensions, @@ -373,25 +353,31 @@ class Hyperopt: def _set_random_state(self, random_state: Optional[int]) -> int: return random_state or random.randint(1, 2**16 - 1) + def prepare_hyperopt_data(self) -> None: + data, timerange = self.backtesting.load_bt_data() + logger.info("Dataload complete. Calculating indicators") + + preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + + # Trim startup period from analyzed dataframe + processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) + + self.min_date, self.max_date = get_timerange(processed) + + logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(self.max_date - self.min_date).days} days)..') + + dump(processed, self.data_pickle_file) + def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 - data, timerange = self.backtesting.load_bt_data() - logger.info("Dataload complete. Calculating indicators") - preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + # Initialize spaces ... + self.init_spaces() - # Trim startup period from analyzed dataframe - for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange, - startup_candles=self.backtesting.required_startup) - min_date, max_date = get_timerange(preprocessed) - - logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') - - dump(preprocessed, self.data_pickle_file) + self.prepare_hyperopt_data() # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange.close() @@ -399,15 +385,12 @@ class Hyperopt: self.backtesting.exchange._api_async = None # type: ignore # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore - self.backtesting.strategy.dp = None # type: ignore - IStrategy.dp = None # type: ignore cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") config_jobs = self.config.get('hyperopt_jobs', -1) logger.info(f'Number of parallel jobs set as: {config_jobs}') - self.dimensions: List[Dimension] = self.hyperopt_space() self.opt = self.get_optimizer(self.dimensions, config_jobs) if self.print_colorized: @@ -473,25 +456,21 @@ class Hyperopt: if is_best: self.current_best_loss = val['loss'] - self.epochs.append(val) + self.current_best_epoch = val - # Save results after each best epoch and every 100 epochs - if is_best or current % 100 == 0: - self._save_results() + self._save_result(val) pbar.update(current) except KeyboardInterrupt: print('User interrupted..') - self._save_results() logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " f"saved to '{self.results_file}'.") - if self.epochs: - sorted_epochs = sorted(self.epochs, key=itemgetter('loss')) - best_epoch = sorted_epochs[0] - HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json) + if self.current_best_epoch: + HyperoptTools.print_epoch_details(self.current_best_epoch, self.total_epochs, + self.print_json) else: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index d4c347f80..ebe405d07 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -3,17 +3,15 @@ import io import logging from collections import OrderedDict from pathlib import Path -from pprint import pformat -from typing import Dict, List +from typing import Any, Dict, List import rapidjson import tabulate from colorama import Fore, Style -from joblib import load from pandas import isna, json_normalize from freqtrade.exceptions import OperationalException -from freqtrade.misc import round_dict +from freqtrade.misc import round_coin_value, round_dict logger = logging.getLogger(__name__) @@ -21,13 +19,38 @@ logger = logging.getLogger(__name__) class HyperoptTools(): + @staticmethod + def has_space(config: Dict[str, Any], space: str) -> bool: + """ + Tell if the space value is contained in the configuration + """ + # The 'trailing' space is not included in the 'default' set of spaces + if space == 'trailing': + return any(s in config['spaces'] for s in [space, 'all']) + else: + return any(s in config['spaces'] for s in [space, 'all', 'default']) + + @staticmethod + def _read_results_pickle(results_file: Path) -> List: + """ + Read hyperopt results from pickle file + LEGACY method - new files are written as json and cannot be read with this method. + """ + from joblib import load + + logger.info(f"Reading pickled epochs from '{results_file}'") + data = load(results_file) + return data + @staticmethod def _read_results(results_file: Path) -> List: """ Read hyperopt results from file """ - logger.info("Reading epochs from '%s'", results_file) - data = load(results_file) + import rapidjson + logger.info(f"Reading epochs from '{results_file}'") + with results_file.open('r') as f: + data = [rapidjson.loads(line) for line in f] return data @staticmethod @@ -37,7 +60,10 @@ class HyperoptTools(): """ epochs: List = [] if results_file.is_file() and results_file.stat().st_size > 0: - epochs = HyperoptTools._read_results(results_file) + if results_file.suffix == '.pickle': + epochs = HyperoptTools._read_results_pickle(results_file) + else: + epochs = HyperoptTools._read_results(results_file) # Detection of some old format, without 'is_best' field saved if epochs[0].get('is_best') is None: raise OperationalException( @@ -53,6 +79,7 @@ class HyperoptTools(): Display details of the hyperopt result """ params = results.get('params_details', {}) + non_optimized = results.get('params_not_optimized', {}) # Default header string if header_str is None: @@ -69,8 +96,10 @@ class HyperoptTools(): print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: - HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") - HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:") + HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", + non_optimized) + HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:", + non_optimized) HyperoptTools._params_pretty_print(params, 'roi', "ROI table:") HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") @@ -96,12 +125,12 @@ class HyperoptTools(): result_dict.update(space_params) @staticmethod - def _params_pretty_print(params, space: str, header: str) -> None: - if space in params: + def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: + if space in params or space in non_optimized: space_params = HyperoptTools._space_params(params, space, 5) - params_result = f"\n# {header}\n" + result = f"\n# {header}\n" if space == 'stoploss': - params_result += f"stoploss = {space_params.get('stoploss')}" + result += f"stoploss = {space_params.get('stoploss')}" elif space == 'roi': # TODO: get rid of OrderedDict when support for python 3.6 will be # dropped (dicts keep the order as the language feature) @@ -110,28 +139,64 @@ class HyperoptTools(): (str(k), v) for k, v in space_params.items() ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" + result += f"minimal_roi = {minimal_roi_result}" elif space == 'trailing': for k, v in space_params.items(): - params_result += f'{k} = {v}\n' + result += f'{k} = {v}\n' else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + no_params = HyperoptTools._space_params(non_optimized, space, 5) - params_result = params_result.replace("\n", "\n ") - print(params_result) + result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}" + + result = result.replace("\n", "\n ") + print(result) @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d + d = params.get(space) + if d: + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + return {} + + @staticmethod + def _pprint(params, non_optimized, indent: int = 4): + """ + Pretty-print hyperopt results (based on 2 dicts - with add. comment) + """ + p = params.copy() + p.update(non_optimized) + result = '{\n' + + for k, param in p.items(): + result += " " * indent + f'"{k}": ' + result += f'"{param}",' if isinstance(param, str) else f'{param},' + if k in non_optimized: + result += " # value loaded from strategy" + result += "\n" + result += '}' + return result @staticmethod def is_best_loss(results, current_best_loss: float) -> bool: - return results['loss'] < current_best_loss + return bool(results['loss'] < current_best_loss) + + @staticmethod + def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str: + """ + Return the formatted results explanation in a string + """ + return (f"{results_metrics['total_trades']:6d} trades. " + f"{results_metrics['wins']}/{results_metrics['draws']}" + f"/{results_metrics['losses']} Wins/Draws/Losses. " + f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " + f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " + f"({results_metrics['profit_total'] * 100: 7.2f}%). " + f"Avg duration {results_metrics['holding_avg']} min." + ) @staticmethod def _format_explanation_string(results, total_epochs) -> str: @@ -156,12 +221,27 @@ class HyperoptTools(): if 'results_metrics.winsdrawslosses' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + legacy_mode = True + + if 'results_metrics.total_trades' in trials: + legacy_mode = False + # New mode, using backtest result for metrics + trials['results_metrics.winsdrawslosses'] = trials.apply( + lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " + f"{x['results_metrics.losses']:>4}", axis=1) + trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.winsdrawslosses', + 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', + 'results_metrics.profit_total', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best']] + else: + # Legacy mode + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', 'Total profit', 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] @@ -171,26 +251,28 @@ class HyperoptTools(): trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Trades'] = trials['Trades'].astype(str) - + perc_multi = 1 if legacy_mode else 100 trials['Epoch'] = trials['Epoch'].apply( lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x * perc_multi:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') ) trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" + if not isna(x) else "--".rjust(7, ' ') ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') ) + stake_currency = config['stake_currency'] trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + lambda x: '{} {}'.format( + round_coin_value(x['Total profit'], stake_currency), + '({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ') + ).rjust(25+len(stake_currency)) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), axis=1 ) trials = trials.drop(columns=['Total profit']) @@ -251,11 +333,21 @@ class HyperoptTools(): trials['Best'] = '' trials['Stake currency'] = config['stake_currency'] - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] + if 'results_metrics.total_trades' in trials: + base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.profit_mean', 'results_metrics.profit_median', + 'results_metrics.profit_total', + 'Stake currency', + 'results_metrics.profit_total_abs', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best'] + perc_multi = 100 + else: + perc_multi = 1 + base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', + 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] @@ -272,21 +364,24 @@ class HyperoptTools(): trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Epoch'] = trials['Epoch'].astype(str) trials['Trades'] = trials['Trades'].astype(str) + trials['Median profit'] = trials['Median profit'] * perc_multi trials['Total profit'] = trials['Total profit'].apply( - lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" + lambda x: f'{x:,.8f}' if x != 0.0 else "" ) trials['Profit'] = trials['Profit'].apply( - lambda x: '{:,.2f}'.format(x) if not isna(x) else "" + lambda x: f'{x:,.2f}' if not isna(x) else "" ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" + lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" ) + if perc_multi == 1: + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: f'{x:,.1f} m' if isinstance( + x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" + ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x) if x != 100000 else "" + lambda x: f'{x:,.5f}' if x != 100000 else "" ) trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a80dc5d31..5822fc627 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -from arrow import Arrow from numpy import int64 from pandas import DataFrame from tabulate import tabulate @@ -44,7 +43,7 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: Generate floatformat (goes in line with _generate_result_line()) """ return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', - '.2f', 'd', 'd', 'd', 'd'] + '.2f', 'd', 's', 's'] def _get_line_header(first_column: str, stake_currency: str) -> List[str]: @@ -53,7 +52,17 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: """ return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Wins', 'Draws', 'Losses'] + 'Win Draw Loss Win%'] + + +def _generate_wins_draws_losses(wins, draws, losses): + if wins > 0 and losses == 0: + wl_ratio = '100' + elif wins == 0: + wl_ratio = '0' + else: + wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' + return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -153,7 +162,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List return tabular_data -def generate_strategy_metrics(all_results: Dict) -> List[Dict]: +def generate_strategy_comparison(all_results: Dict) -> List[Dict]: """ Generate summary per strategy :param all_results: Dict of containing results for all strategies @@ -165,6 +174,17 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]: tabular_data.append(_generate_result_line( results['results'], results['config']['dry_run_wallet'], strategy) ) + try: + max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'], + value_col='profit_ratio') + max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(results['results'], + value_col='profit_abs') + except ValueError: + max_drawdown_per = 0 + max_drawdown_abs = 0 + tabular_data[-1]['max_drawdown_per'] = round(max_drawdown_per * 100, 2) + tabular_data[-1]['max_drawdown_abs'] = \ + round_coin_value(max_drawdown_abs, results['config']['stake_currency'], False) return tabular_data @@ -194,7 +214,40 @@ def generate_edge_table(results: dict) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore +def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: + """ Generate overall trade statistics """ + if len(results) == 0: + return { + 'wins': 0, + 'losses': 0, + 'draws': 0, + 'holding_avg': timedelta(), + 'winner_holding_avg': timedelta(), + 'loser_holding_avg': timedelta(), + } + + winning_trades = results.loc[results['profit_ratio'] > 0] + draw_trades = results.loc[results['profit_ratio'] == 0] + losing_trades = results.loc[results['profit_ratio'] < 0] + zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) & + (results['sell_reason'] == 'trailing_stop_loss')]) + + return { + 'wins': len(winning_trades), + 'losses': len(losing_trades), + 'draws': len(draw_trades), + 'holding_avg': (timedelta(minutes=round(results['trade_duration'].mean())) + if not results.empty else timedelta()), + 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) + if not winning_trades.empty else timedelta()), + 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) + if not losing_trades.empty else timedelta()), + 'zero_duration_trades': zero_duration_trades, + } + + def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: + """ Generate daily statistics """ if len(results) == 0: return { 'backtest_best_day': 0, @@ -204,8 +257,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': 0, 'draw_days': 0, 'losing_days': 0, - 'winner_holding_avg': timedelta(), - 'loser_holding_avg': timedelta(), } daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) @@ -217,9 +268,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: draw_days = sum(daily_profit == 0) losing_days = sum(daily_profit < 0) - winning_trades = results.loc[results['profit_ratio'] > 0] - losing_trades = results.loc[results['profit_ratio'] < 0] - return { 'backtest_best_day': best_rel, 'backtest_worst_day': worst_rel, @@ -228,16 +276,152 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, - 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) - if not winning_trades.empty else timedelta()), - 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) - if not losing_trades.empty else timedelta()), } +def generate_strategy_stats(btdata: Dict[str, DataFrame], + strategy: str, + content: Dict[str, Any], + min_date: datetime, max_date: datetime, + market_change: float + ) -> Dict[str, Any]: + """ + :param btdata: Backtest data + :param strategy: Strategy name + :param content: Backtest result data in the format: + {'results: results, 'config: config}}. + :param min_date: Backtest start date + :param max_date: Backtest end date + :param market_change: float indicating the market change + :return: Dictionary containing results per strategy and a stratgy summary. + """ + results: Dict[str, DataFrame] = content['results'] + if not isinstance(results, DataFrame): + return {} + config = content['config'] + max_open_trades = min(config['max_open_trades'], len(btdata.keys())) + starting_balance = config['dry_run_wallet'] + stake_currency = config['stake_currency'] + + pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results, skip_nan=False) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, + results=results) + left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results.loc[results['is_open']], + skip_nan=True) + daily_stats = generate_daily_stats(results) + trade_stats = generate_trading_stats(results) + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 + results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 + + backtest_days = (max_date - min_date).days + strat_stats = { + 'trades': results.to_dict(orient='records'), + 'locks': [lock.to_json() for lock in content['locks']], + 'best_pair': best_pair, + 'worst_pair': worst_pair, + 'results_per_pair': pair_results, + 'sell_reason_summary': sell_reason_stats, + 'left_open_trades': left_open_results, + 'total_trades': len(results), + 'total_volume': float(results['stake_amount'].sum()), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, + 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, + 'profit_total': results['profit_abs'].sum() / starting_balance, + 'profit_total_abs': results['profit_abs'].sum(), + 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), + 'backtest_start_ts': int(min_date.timestamp() * 1000), + 'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT), + 'backtest_end_ts': int(max_date.timestamp() * 1000), + 'backtest_days': backtest_days, + + 'backtest_run_start_ts': content['backtest_start_time'], + 'backtest_run_end_ts': content['backtest_end_time'], + + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, + 'market_change': market_change, + 'pairlist': list(btdata.keys()), + 'stake_amount': config['stake_amount'], + 'stake_currency': config['stake_currency'], + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), + 'starting_balance': starting_balance, + 'dry_run_wallet': starting_balance, + 'final_balance': content['final_balance'], + 'rejected_signals': content['rejected_signals'], + 'max_open_trades': max_open_trades, + 'max_open_trades_setting': (config['max_open_trades'] + if config['max_open_trades'] != float('inf') else -1), + 'timeframe': config['timeframe'], + 'timerange': config.get('timerange', ''), + 'enable_protections': config.get('enable_protections', False), + 'strategy_name': strategy, + # Parameters relevant for backtesting + 'stoploss': config['stoploss'], + 'trailing_stop': config.get('trailing_stop', False), + 'trailing_stop_positive': config.get('trailing_stop_positive'), + 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), + 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), + 'use_custom_stoploss': config.get('use_custom_stoploss', False), + 'minimal_roi': config['minimal_roi'], + 'use_sell_signal': config['ask_strategy']['use_sell_signal'], + 'sell_profit_only': config['ask_strategy']['sell_profit_only'], + 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], + 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], + **daily_stats, + **trade_stats + } + + try: + max_drawdown, _, _, _, _ = calculate_max_drawdown( + results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') + strat_stats.update({ + 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, + 'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT), + 'drawdown_start_ts': drawdown_start.timestamp() * 1000, + 'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT), + 'drawdown_end_ts': drawdown_end.timestamp() * 1000, + + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, + }) + + csum_min, csum_max = calculate_csum(results, starting_balance) + strat_stats.update({ + 'csum_min': csum_min, + 'csum_max': csum_max + }) + + except ValueError: + strat_stats.update({ + 'max_drawdown': 0.0, + 'max_drawdown_abs': 0.0, + 'max_drawdown_low': 0.0, + 'max_drawdown_high': 0.0, + 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_start_ts': 0, + 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_end_ts': 0, + 'csum_min': 0, + 'csum_max': 0 + }) + + return strat_stats + + def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], - min_date: Arrow, max_date: Arrow + min_date: datetime, max_date: datetime ) -> Dict[str, Any]: """ :param btdata: Backtest data @@ -245,132 +429,17 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date - :return: - Dictionary containing results per strategy and a stratgy summary. + :return: Dictionary containing results per strategy and a stratgy summary. """ result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, content in all_results.items(): - results: Dict[str, DataFrame] = content['results'] - if not isinstance(results, DataFrame): - continue - config = content['config'] - max_open_trades = min(config['max_open_trades'], len(btdata.keys())) - starting_balance = config['dry_run_wallet'] - stake_currency = config['stake_currency'] - - pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, - results=results) - left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results.loc[results['is_open']], - skip_nan=True) - daily_stats = generate_daily_stats(results) - best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], - key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], - key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 - results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 - - backtest_days = (max_date - min_date).days - strat_stats = { - 'trades': results.to_dict(orient='records'), - 'locks': [lock.to_json() for lock in content['locks']], - 'best_pair': best_pair, - 'worst_pair': worst_pair, - 'results_per_pair': pair_results, - 'sell_reason_summary': sell_reason_stats, - 'left_open_trades': left_open_results, - 'total_trades': len(results), - 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, - 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_abs'].sum() / starting_balance, - 'profit_total_abs': results['profit_abs'].sum(), - 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.int_timestamp * 1000, - 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.int_timestamp * 1000, - 'backtest_days': backtest_days, - - 'backtest_run_start_ts': content['backtest_start_time'], - 'backtest_run_end_ts': content['backtest_end_time'], - - 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, - 'market_change': market_change, - 'pairlist': list(btdata.keys()), - 'stake_amount': config['stake_amount'], - 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), - 'starting_balance': starting_balance, - 'dry_run_wallet': starting_balance, - 'final_balance': content['final_balance'], - 'max_open_trades': max_open_trades, - 'max_open_trades_setting': (config['max_open_trades'] - if config['max_open_trades'] != float('inf') else -1), - 'timeframe': config['timeframe'], - 'timerange': config.get('timerange', ''), - 'enable_protections': config.get('enable_protections', False), - 'strategy_name': strategy, - # Parameters relevant for backtesting - 'stoploss': config['stoploss'], - 'trailing_stop': config.get('trailing_stop', False), - 'trailing_stop_positive': config.get('trailing_stop_positive'), - 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), - 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), - 'use_custom_stoploss': config.get('use_custom_stoploss', False), - 'minimal_roi': config['minimal_roi'], - 'use_sell_signal': config['ask_strategy']['use_sell_signal'], - 'sell_profit_only': config['ask_strategy']['sell_profit_only'], - 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], - 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], - **daily_stats, - } + strat_stats = generate_strategy_stats(btdata, strategy, content, + min_date, max_date, market_change=market_change) result['strategy'][strategy] = strat_stats - try: - max_drawdown, _, _, _, _ = calculate_max_drawdown( - results, value_col='profit_ratio') - drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( - results, value_col='profit_abs') - strat_stats.update({ - 'max_drawdown': max_drawdown, - 'max_drawdown_abs': drawdown_abs, - 'drawdown_start': drawdown_start, - 'drawdown_start_ts': drawdown_start.timestamp() * 1000, - 'drawdown_end': drawdown_end, - 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, - }) - - csum_min, csum_max = calculate_csum(results, starting_balance) - strat_stats.update({ - 'csum_min': csum_min, - 'csum_max': csum_max - }) - - except ValueError: - strat_stats.update({ - 'max_drawdown': 0.0, - 'max_drawdown_abs': 0.0, - 'max_drawdown_low': 0.0, - 'max_drawdown_high': 0.0, - 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_start_ts': 0, - 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_end_ts': 0, - 'csum_min': 0, - 'csum_max': 0 - }) - - strategy_results = generate_strategy_metrics(all_results=all_results) + strategy_results = generate_strategy_comparison(all_results=all_results) result['strategy_comparison'] = strategy_results @@ -393,7 +462,8 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st floatfmt = _get_line_floatfmt(stake_currency) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) ] for t in pair_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, @@ -410,9 +480,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren headers = [ 'Sell Reason', 'Sells', - 'Wins', - 'Draws', - 'Losses', + 'Win Draws Loss Win%', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', @@ -420,7 +488,8 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] output = [[ - t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], + t['sell_reason'], t['trades'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), t['profit_mean_pct'], t['profit_sum_pct'], round_coin_value(t['profit_total_abs'], stake_currency, False), t['profit_total_pct'], @@ -438,11 +507,22 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: """ floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header('Strategy', stake_currency) + # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless + # therefore we slip this column in only for strategy summary here. + headers.append('Drawdown') + + # Align drawdown string on the center two space separator. + drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results] + dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results]) + dd_pad_per = max([len(dd) for dd in drawdown]) + drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%' + for t, dd in zip(strategy_results, drawdown)] output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] - ] for t in strategy_results] + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown] + for t, drawdown in zip(strategy_results, drawdown)] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") @@ -452,9 +532,21 @@ def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio']) worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio']) + + # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show + # command stores these results and newer version of freqtrade must be able to handle old + # results with missing new fields. + zero_duration_trades = '--' + + if 'zero_duration_trades' in strat_results: + zero_duration_trades_per = \ + 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] + zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \ + f'({strat_results["zero_duration_trades"]})' + metrics = [ - ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), - ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Backtesting from', strat_results['backtest_start']), + ('Backtesting to', strat_results['backtest_end']), ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), @@ -464,13 +556,12 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), + ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"), ('Trades per day', strat_results['trades_per_day']), ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), - ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), @@ -488,6 +579,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), + ('Zero Duration Trades', zero_duration_trades), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], @@ -502,8 +595,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], strat_results['stake_currency'])), - ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), - ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Drawdown Start', strat_results['drawdown_start']), + ('Drawdown End', strat_results['drawdown_end']), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), ] @@ -522,37 +615,43 @@ def text_table_add_metrics(strat_results: Dict) -> str: return message +def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str): + """ + Print results for one strategy + """ + # Print results + print(f"Result for strategy {strategy}") + table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) + if isinstance(table, str): + print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], + stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_add_metrics(results) + if isinstance(table, str) and len(table) > 0: + print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) + print(table) + + if isinstance(table, str) and len(table) > 0: + print('=' * len(table.splitlines()[0])) + print() + + def show_backtest_results(config: Dict, backtest_stats: Dict): stake_currency = config['stake_currency'] for strategy, results in backtest_stats['strategy'].items(): - - # Print results - print(f"Result for strategy {strategy}") - table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) - if isinstance(table, str): - print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], - stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_add_metrics(results) - if isinstance(table, str) and len(table) > 0: - print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) - print(table) - - if isinstance(table, str) and len(table) > 0: - print('=' * len(table.splitlines()[0])) - print() + show_backtest_result(strategy, results, stake_currency) if len(backtest_stats['strategy']) > 1: # Print Strategy summary table diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 961363b0e..d89256baf 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -123,6 +123,27 @@ def migrate_open_orders_to_trades(engine): """) +def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): + # Schema migration necessary + engine.execute(f"alter table orders rename to {table_back_name}") + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + engine.execute(f"drop index {index['name']}") + + # let SQLAlchemy create the schema as required + decl_base.metadata.create_all(engine) + + engine.execute(f""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, average, remaining, cost, order_date, + order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date, + order_filled_date, order_update_date + from {table_back_name} + """) + + def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary @@ -145,6 +166,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: - pass - # Empty for now - as there is only one iteration of the orders table so far. - # table_back_name = get_backup_name(tabs, 'orders_bak') + cols_order = inspector.get_columns('orders') + + if not has_column(cols_order, 'average'): + tabs = get_table_names_for_table(inspector, 'orders') + # Empty for now - as there is only one iteration of the orders table so far. + table_back_name = get_backup_name(tabs, 'orders_bak') + + migrate_orders_table(decl_base, inspector, engine, table_back_name, cols) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index afd51366a..f2e7a10c4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -112,16 +112,17 @@ class Order(_DECL_BASE): trade = relationship("Trade", back_populates="orders") - ft_order_side = Column(String, nullable=False) - ft_pair = Column(String, nullable=False) + ft_order_side = Column(String(25), nullable=False) + ft_pair = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) - order_id = Column(String, nullable=False, index=True) - status = Column(String, nullable=True) - symbol = Column(String, nullable=True) - order_type = Column(String, nullable=True) - side = Column(String, nullable=True) + order_id = Column(String(255), nullable=False, index=True) + status = Column(String(255), nullable=True) + symbol = Column(String(25), nullable=True) + order_type = Column(String(50), nullable=True) + side = Column(String(25), nullable=True) price = Column(Float, nullable=True) + average = Column(Float, nullable=True) amount = Column(Float, nullable=True) filled = Column(Float, nullable=True) remaining = Column(Float, nullable=True) @@ -150,6 +151,7 @@ class Order(_DECL_BASE): self.price = order.get('price', self.price) self.amount = order.get('amount', self.amount) self.filled = order.get('filled', self.filled) + self.average = order.get('average', self.average) self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) if 'timestamp' in order and order['timestamp'] is not None: @@ -656,15 +658,15 @@ class Trade(_DECL_BASE, LocalTrade): orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") - exchange = Column(String, nullable=False) - pair = Column(String, nullable=False, index=True) + exchange = Column(String(25), nullable=False) + pair = Column(String(25), nullable=False, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True) fee_open = Column(Float, nullable=False, default=0.0) fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String, nullable=True) + fee_open_currency = Column(String(25), nullable=True) fee_close = Column(Float, nullable=False, default=0.0) fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String, nullable=True) + fee_close_currency = Column(String(25), nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) # open_trade_value - calculated via _calc_open_trade_value @@ -678,7 +680,7 @@ class Trade(_DECL_BASE, LocalTrade): amount_requested = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) - open_order_id = Column(String) + open_order_id = Column(String(255)) # absolute value of the stop loss stop_loss = Column(Float, nullable=True, default=0.0) # percentage value of the stop loss @@ -688,16 +690,16 @@ class Trade(_DECL_BASE, LocalTrade): # percentage value of the initial stop loss initial_stop_loss_pct = Column(Float, nullable=True) # stoploss order id which is on exchange - stoploss_order_id = Column(String, nullable=True, index=True) + stoploss_order_id = Column(String(255), nullable=True, index=True) # last update time of the stoploss order on exchange stoploss_last_update = Column(DateTime, nullable=True) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String, nullable=True) - sell_order_status = Column(String, nullable=True) - strategy = Column(String, nullable=True) + sell_reason = Column(String(100), nullable=True) + sell_order_status = Column(String(100), nullable=True) + strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): @@ -815,18 +817,20 @@ class Trade(_DECL_BASE, LocalTrade): pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') ).filter(Trade.is_open.is_(False))\ .group_by(Trade.pair) \ - .order_by(desc('profit_sum')) \ + .order_by(desc('profit_sum_abs')) \ .all() return [ { 'pair': pair, - 'profit': rate, + 'profit': profit, + 'profit_abs': profit_abs, 'count': count } - for pair, rate, count in pair_rates + for pair, profit, profit_abs, count in pair_rates ] @staticmethod @@ -852,8 +856,8 @@ class PairLock(_DECL_BASE): id = Column(Integer, primary_key=True) - pair = Column(String, nullable=False, index=True) - reason = Column(String, nullable=True) + pair = Column(String(25), nullable=False, index=True) + reason = Column(String(255), nullable=True) # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d5a729ee1..bb4283406 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -77,7 +77,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): ) except ValueError as e: raise OperationalException(e) from e - trades = trim_dataframe(trades, timerange, 'open_date') + if not trades.empty: + trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, "trades": trades, @@ -540,8 +541,11 @@ def load_and_plot_trades(config: Dict[str, Any]): df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) df_analyzed = trim_dataframe(df_analyzed, timerange) - trades_pair = trades.loc[trades['pair'] == pair] - trades_pair = extract_trades_of_period(df_analyzed, trades_pair) + if not trades.empty: + trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = extract_trades_of_period(df_analyzed, trades_pair) + else: + trades_pair = trades fig = generate_candlestick_graph( pair=pair, diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 8a5379ca6..8f623b062 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -71,14 +71,14 @@ class AgeFilter(IPairList): daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) - logger.info(f"Validated {len(pairlist)} pairs.") + self.log_once(f"Validated {len(pairlist)} pairs.", logger.info) return pairlist def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: """ Validate age for the ticker :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache @@ -86,7 +86,7 @@ class AgeFilter(IPairList): return True if daily_candles is not None: - if len(daily_candles) > self._min_days_listed: + if len(daily_candles) >= self._min_days_listed: # We have fetched at least the minimum required number of daily candles # Add to cache, store the time we last checked this symbol self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000 diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 3e6252fc4..74348b1a7 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -7,7 +7,7 @@ from copy import deepcopy from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.exchange import market_is_active +from freqtrade.exchange import Exchange, market_is_active from freqtrade.mixins import LoggingMixin @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class IPairList(LoggingMixin, ABC): - def __init__(self, exchange, pairlistmanager, + def __init__(self, exchange: Exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: """ @@ -28,7 +28,7 @@ class IPairList(LoggingMixin, ABC): """ self._enabled = True - self._exchange = exchange + self._exchange: Exchange = exchange self._pairlistmanager = pairlistmanager self._config = config self._pairlistconfig = pairlistconfig @@ -68,7 +68,7 @@ class IPairList(LoggingMixin, ABC): filter_pairlist() method. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ raise NotImplementedError() diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 73a9436fa..bf474cb21 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -39,7 +39,12 @@ class PerformanceFilter(IPairList): :return: new allowlist """ # Get the trading performance for pairs from database - performance = pd.DataFrame(Trade.get_overall_performance()) + try: + performance = pd.DataFrame(Trade.get_overall_performance()) + except AttributeError: + # Performancefilter does not work in backtesting. + self.log_once("PerformanceFilter is not available in this mode.", logger.warning) + return pairlist # Skip performance-based sorting if no performance data is available if len(performance) == 0: diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index 519337f29..a3c262e8c 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -48,7 +48,7 @@ class PrecisionFilter(IPairList): Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very low value pairs. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ stop_price = ticker['ask'] * self._stoploss diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index a0579b196..5b5afb557 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -27,9 +27,13 @@ class PriceFilter(IPairList): self._max_price = pairlistconfig.get('max_price', 0) if self._max_price < 0: raise OperationalException("PriceFilter requires max_price to be >= 0") + self._max_value = pairlistconfig.get('max_value', 0) + if self._max_value < 0: + raise OperationalException("PriceFilter requires max_value to be >= 0") self._enabled = ((self._low_price_ratio > 0) or (self._min_price > 0) or - (self._max_price > 0)) + (self._max_price > 0) or + (self._max_value > 0)) @property def needstickers(self) -> bool: @@ -51,6 +55,8 @@ class PriceFilter(IPairList): active_price_filters.append(f"below {self._min_price:.8f}") if self._max_price != 0: active_price_filters.append(f"above {self._max_price:.8f}") + if self._max_value != 0: + active_price_filters.append(f"Value above {self._max_value:.8f}") if len(active_price_filters): return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." @@ -61,7 +67,7 @@ class PriceFilter(IPairList): """ Check if if one price-step (pip) is > than a certain barrier. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ if ticker.get('last', None) is None or ticker.get('last') == 0: @@ -79,6 +85,32 @@ class PriceFilter(IPairList): f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False + # Perform low_amount check + if self._max_value != 0: + price = ticker['last'] + market = self._exchange.markets[pair] + limits = market['limits'] + if ('amount' in limits and 'min' in limits['amount'] + and limits['amount']['min'] is not None): + min_amount = limits['amount']['min'] + min_precision = market['precision']['amount'] + + min_value = min_amount * price + if self._exchange.precisionMode == 4: + # tick size + next_value = (min_amount + min_precision) * price + else: + # Decimal places + min_precision = pow(0.1, min_precision) + next_value = (min_amount + min_precision) * price + diff = next_value - min_value + + if diff > self._max_value: + self.log_once(f"Removed {pair} from whitelist, " + f"because min value change of {diff} > {self._max_value}.", + logger.info) + return False + # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: @@ -89,7 +121,7 @@ class PriceFilter(IPairList): # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because last price > {self._max_price:.8f}", logger.info) return False diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 9fa211750..1b152774b 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -40,7 +40,7 @@ class SpreadFilter(IPairList): """ Validate spread for the ticker :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ if 'bid' in ticker and 'ask' in ticker and ticker['ask']: diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 400b1577d..bc617a1db 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -90,7 +90,7 @@ class VolatilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 6565e92c1..8be61166b 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -83,7 +83,7 @@ class RangeStabilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index e582f6aa8..4d06d3ecf 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -57,6 +57,7 @@ class Count(BaseModel): class PerformanceEntry(BaseModel): pair: str profit: float + profit_abs: float count: int @@ -268,7 +269,7 @@ class DeleteTrade(BaseModel): class PlotConfig_(BaseModel): main_plot: Dict[str, Any] - subplots: Optional[Dict[str, Any]] + subplots: Dict[str, Any] class PlotConfig(BaseModel): diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 380070deb..5ae20afa1 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -3,11 +3,13 @@ Module that define classes to convert Crypto-currency to FIAT e.g BTC to USD """ +import datetime import logging from typing import Dict from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI +from requests.exceptions import RequestException from freqtrade.constants import SUPPORTED_FIAT @@ -25,6 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} + _backoff: float = 0.0 def __new__(cls): """ @@ -47,8 +50,21 @@ class CryptoToFiatConverter: def _load_cryptomap(self) -> None: try: coinlistings = self._coingekko.get_coins_list() - # Create mapping table from synbol to coingekko_id + # Create mapping table from symbol to coingekko_id self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} + except RequestException as request_exception: + if "429" in str(request_exception): + logger.warning( + "Too many requests for Coingecko API, backing off and trying again later.") + # Set backoff timestamp to 60 seconds in the future + self._backoff = datetime.datetime.now().timestamp() + 60 + return + # If the request is not a 429 error we want to raise the normal error + logger.error( + "Could not load FIAT Cryptocurrency map for the following problem: {}".format( + request_exception + ) + ) except (Exception) as exception: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") @@ -127,6 +143,15 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 + if self._cryptomap == {}: + if self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + # return 0.0 if we still dont have data to check, no reason to proceed + if self._cryptomap == {}: + return 0.0 + else: + return 0.0 + if crypto_symbol not in self._cryptomap: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fd97ad7d4..3f26619a9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -178,7 +178,7 @@ class RPC: current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) - + current_profit_fiat: Optional[float] = None # Calculate fiat profit if self._fiat_converter: current_profit_fiat = self._fiat_converter.convert_amount( @@ -220,12 +220,13 @@ class RPC: return results def _rpc_status_table(self, stake_currency: str, - fiat_display_currency: str) -> Tuple[List, List]: + fiat_display_currency: str) -> Tuple[List, List, float]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: trades_list = [] + fiat_profit_sum = NAN for trade in trades: # calculate profit and send message to user try: @@ -243,6 +244,8 @@ class RPC: ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" + fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ + else fiat_profit_sum + fiat_profit trades_list.append([ trade.id, trade.pair + ('*' if (trade.open_order_id is not None @@ -256,7 +259,7 @@ class RPC: profitcol += " (" + fiat_display_currency + ")" columns = ['ID', 'Pair', 'Since', profitcol] - return trades_list, columns + return trades_list, columns, fiat_profit_sum def _rpc_daily_profit( self, timescale: int, @@ -845,5 +848,7 @@ class RPC: df_analyzed, arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: - + if (self._freqtrade.strategy.plot_config and + 'subplots' not in self._freqtrade.strategy.plot_config): + self._freqtrade.strategy.plot_config['subplots'] = {} return self._freqtrade.strategy.plot_config diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3eeedcd12..cca87ad91 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -8,19 +8,21 @@ import logging from datetime import timedelta from html import escape from itertools import chain -from typing import Any, Callable, Dict, List, Union +from math import isnan +from typing import Any, Callable, Dict, List, Optional, Union, cast import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update +from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode, + ReplyKeyboardMarkup, Update) from telegram.error import NetworkError, TelegramError -from telegram.ext import CallbackContext, CommandHandler, Updater +from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN from freqtrade.exceptions import OperationalException -from freqtrade.misc import round_coin_value +from freqtrade.misc import chunks, round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -87,7 +89,7 @@ class Telegram(RPCHandler): Validates the keyboard configuration from telegram config section. """ - self._keyboard: List[List[Union[str, KeyboardButton]]] = [ + self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] @@ -169,6 +171,11 @@ class Telegram(RPCHandler): [h.command for h in handles] ) + self._current_callback_query_handler: Optional[CallbackQueryHandler] = None + self._callback_query_handlers = { + 'forcebuy': CallbackQueryHandler(self._forcebuy_inline) + } + def cleanup(self) -> None: """ Stops all running telegram threads. @@ -226,44 +233,58 @@ class Telegram(RPCHandler): def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ - noti = self._config['telegram'].get('notification_settings', {} - ).get(str(msg['type']), 'on') + default_noti = 'on' + + msg_type = msg['type'] + noti = '' + if msg_type == RPCMessageType.SELL: + sell_noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), {}) + # For backward compatibility sell still be string + if isinstance(noti, str): + noti = sell_noti + else: + noti = sell_noti.get(str(msg['sell_reason']), default_noti) + else: + noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), default_noti) + if noti == 'off': - logger.info(f"Notification '{msg['type']}' not sent.") + logger.info(f"Notification '{msg_type}' not sent.") # Notification disabled return - if msg['type'] == RPCMessageType.BUY: + if msg_type == RPCMessageType.BUY: message = self._format_buy_msg(msg) - elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): - msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell' + elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell' message = ("\N{WARNING SIGN} *{exchange}:* " "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == RPCMessageType.BUY_FILL: + elif msg_type == RPCMessageType.BUY_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " "Buy order for {pair} (#{trade_id}) filled " "for {open_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL_FILL: + elif msg_type == RPCMessageType.SELL_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " "Sell order for {pair} (#{trade_id}) filled " "for {close_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL: + elif msg_type == RPCMessageType.SELL: message = self._format_sell_msg(msg) - elif msg['type'] == RPCMessageType.STATUS: + elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.WARNING: + elif msg_type == RPCMessageType.WARNING: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.STARTUP: + elif msg_type == RPCMessageType.STARTUP: message = '{status}'.format(**msg) else: - raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) + raise NotImplementedError('Unknown message type: {}'.format(msg_type)) self._send_msg(message, disable_notification=(noti == 'silent')) @@ -354,19 +375,31 @@ class Telegram(RPCHandler): :return: None """ try: - statlist, head = self._rpc._rpc_status_table( - self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + fiat_currency = self._config.get('fiat_display_currency', '') + statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( + self._config['stake_currency'], fiat_currency) + show_total = not isnan(fiat_profit_sum) and len(statlist) > 1 max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message 0.99 is used to make sure that there are no extra (empty) messages As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message """ - for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): - message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], + messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1) + for i in range(0, messages_count): + trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg] + if show_total and i == messages_count - 1: + # append total line + trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"]) + + message = tabulate(trades, headers=head, tablefmt='simple') + if show_total and i == messages_count - 1: + # insert separators line between Total + lines = message.split("\n") + message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) @@ -624,6 +657,25 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + def _forcebuy_action(self, pair, price=None): + try: + self._rpc._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) + + def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: + if update.callback_query: + query = update.callback_query + pair = query.data + query.answer() + query.edit_message_text(text=f"Force Buying: {pair}") + self._forcebuy_action(pair) + + @staticmethod + def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], + cols=3) -> List[List[InlineKeyboardButton]]: + return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] + @authorized_only def _forcebuy(self, update: Update, context: CallbackContext) -> None: """ @@ -636,10 +688,13 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - try: - self._rpc._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + self._forcebuy_action(pair, price) + else: + whitelist = self._rpc._rpc_whitelist()['whitelist'] + pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist] + self._send_inline_msg("Which pair?", + keyboard=self._layout_inline_keyboard(pairs), + callback_query_handler='forcebuy') @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -711,8 +766,11 @@ class Telegram(RPCHandler): trades = self._rpc._rpc_performance() output = "Performance:\n" for i, trade in enumerate(trades): - stat_line = (f"{i+1}.\t {trade['pair']}\t{trade['profit']:.2f}% " - f"({trade['count']})\n") + stat_line = ( + f"{i+1}.\t {trade['pair']}\t" + f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " + f"({trade['profit']:.2f}%) " + f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: self._send_msg(output, parse_mode=ParseMode.HTML) @@ -750,17 +808,21 @@ class Telegram(RPCHandler): Handler for /locks. Returns the currently active locks """ - locks = self._rpc._rpc_locks() - message = tabulate([[ - lock['id'], - lock['pair'], - lock['lock_end_time'], - lock['reason']] for lock in locks['locks']], - headers=['ID', 'Pair', 'Until', 'Reason'], - tablefmt='simple') - message = f"
{escape(message)}
" - logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + rpc_locks = self._rpc._rpc_locks() + if not rpc_locks['locks']: + self._send_msg('No active locks.', parse_mode=ParseMode.HTML) + + for locks in chunks(rpc_locks['locks'], 25): + message = tabulate([[ + lock['id'], + lock['pair'], + lock['lock_end_time'], + lock['reason']] for lock in locks], + headers=['ID', 'Pair', 'Until', 'Reason'], + tablefmt='simple') + message = f"
{escape(message)}
" + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only def _delete_locks(self, update: Update, context: CallbackContext) -> None: @@ -860,9 +922,17 @@ class Telegram(RPCHandler): """ try: edge_pairs = self._rpc._rpc_edge() - edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') - message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML) + if not edge_pairs: + message = 'Edge only validated following pairs:' + self._send_msg(message, parse_mode=ParseMode.HTML) + + for chunk in chunks(edge_pairs, 25): + edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') + message = (f'Edge only validated following pairs:\n' + f'
{edge_pairs_tab}
') + + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: self._send_msg(str(e)) @@ -959,8 +1029,9 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False) -> None: + def _send_inline_msg(self, msg: str, callback_query_handler, + parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, + keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: """ Send given markdown message :param msg: message @@ -968,7 +1039,29 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) + if self._current_callback_query_handler: + self._updater.dispatcher.remove_handler(self._current_callback_query_handler) + self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] + self._updater.dispatcher.add_handler(self._current_callback_query_handler) + + self._send_msg(msg, parse_mode, disable_notification, + cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard), + reply_markup=InlineKeyboardMarkup) + + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, + disable_notification: bool = False, + keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None, + reply_markup=ReplyKeyboardMarkup) -> None: + """ + Send given markdown message + :param msg: message + :param bot: alternative bot + :param parse_mode: telegram parse mode + :return: None + """ + if keyboard is None: + keyboard = self._keyboard + reply_markup = reply_markup(keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32486136d..7dee47d87 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -5,7 +5,9 @@ This module defines a base class for auto-hyperoptable strategies. import logging from abc import ABC, abstractmethod from contextlib import suppress -from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union + +from freqtrade.optimize.hyperopt_tools import HyperoptTools with suppress(ImportError): @@ -26,7 +28,8 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - hyperopt: bool = False + in_space: bool = False + name: str def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -131,7 +134,7 @@ class IntParameter(NumericParameter): Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid calculating 100ds of indicators. """ - if self.hyperopt: + if self.in_space and self.optimize: # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive return range(self.low, self.high + 1) else: @@ -247,6 +250,10 @@ class HyperStrategyMixin(object): """ Initialize hyperoptable strategy mixin. """ + self.config = config + self.ft_buy_params: List[BaseParameter] = [] + self.ft_sell_params: List[BaseParameter] = [] + self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: @@ -257,15 +264,26 @@ class HyperStrategyMixin(object): """ if category not in ('buy', 'sell', None): raise OperationalException('Category must be one of: "buy", "sell", None.') + + if category is None: + params = self.ft_buy_params + self.ft_sell_params + else: + params = getattr(self, f"ft_{category}_params") + + for par in params: + yield par.name, par + + def _detect_parameters(self, category: str) -> Iterator[Tuple[str, BaseParameter]]: + """ Detect all parameters for 'category' """ for attr_name in dir(self): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): - if (category and attr_name.startswith(category + '_') + if (attr_name.startswith(category + '_') and attr.category is not None and attr.category != category): raise OperationalException( f'Inconclusive parameter name {attr_name}, category: {attr.category}.') - if (category is None or category == attr.category or + if (category == attr.category or (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr @@ -283,9 +301,16 @@ class HyperStrategyMixin(object): """ if not params: logger.info(f"No params for {space} found, using default values.") + param_container: List[BaseParameter] = getattr(self, f"ft_{space}_params") + + for attr_name, attr in self._detect_parameters(space): + attr.name = attr_name + attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) + if not attr.category: + attr.category = space + + param_container.append(attr) - for attr_name, attr in self.enumerate_parameters(): - attr.hyperopt = hyperopt if params and attr_name in params: if attr.load: attr.value = params[attr_name] @@ -295,3 +320,16 @@ class HyperStrategyMixin(object): f'Default value "{attr.value}" used.') else: logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') + + def get_params_dict(self): + """ + Returns list of Parameters that are not part of the current optimize job + """ + params = { + 'buy': {}, + 'sell': {} + } + for name, p in self.enumerate_parameters(): + if not p.optimize or not p.in_space: + params[p.category][name] = p.value + return params diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 645e70e8a..e2cde52eb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -161,6 +161,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ + return dataframe @abstractmethod def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -170,6 +171,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ + return dataframe @abstractmethod def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -179,6 +181,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: DataFrame with sell column """ + return dataframe def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ @@ -226,7 +229,7 @@ class IStrategy(ABC, HyperStrategyMixin): pass def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, **kwargs) -> bool: + time_in_force: str, current_time: datetime, **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -241,6 +244,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param amount: Amount in target (quote) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process @@ -248,7 +252,8 @@ class IStrategy(ABC, HyperStrategyMixin): return True def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: + rate: float, time_in_force: str, sell_reason: str, + current_time: datetime, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or @@ -267,6 +272,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param sell_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the sell-order is placed on the exchange. False aborts the process @@ -274,7 +280,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: DataFrame, **kwargs) -> float: + current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -290,15 +296,13 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: DataFrame, - **kwargs) -> Optional[Union[str, bool]]: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ Custom sell signal logic indicating that specified position should be sold. Returning a string or True from this method is equal to setting sell signal on a candle at specified @@ -536,8 +540,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, - buy: bool, sell: bool, low: float = None, high: float = None, + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell @@ -553,9 +557,8 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate, - trade=trade, current_time=date, - current_profit=current_profit, + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) # Set current rate to high for backtesting sell @@ -570,6 +573,10 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.NONE custom_reason = '' + # use provided rate in backtesting, not high/low. + current_rate = rate + current_profit = trade.calc_profit_ratio(current_rate) + if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal @@ -580,7 +587,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, - current_profit=current_profit, dataframe=dataframe) + current_profit=current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -617,7 +624,7 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, + def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ @@ -635,8 +642,7 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, - current_profit=current_profit, - dataframe=dataframe) + current_profit=current_profit) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 42f088f9f..8933ebc6a 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -9,7 +9,8 @@ "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, - "sell": 30 + "sell": 30, + "unit": "minutes" }, "bid_strategy": { "price_side": "bid", diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index c69b52cad..2a9ac0690 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -39,7 +39,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', return self.stoploss def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, **kwargs) -> bool: + time_in_force: str, current_time: 'datetime', **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -54,6 +54,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f :param amount: Amount in target (quote) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process @@ -61,7 +62,8 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f return True def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: + rate: float, time_in_force: str, sell_reason: str, + current_time: 'datetime', **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or @@ -80,6 +82,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: :param sell_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the sell-order is placed on the exchange. False aborts the process diff --git a/requirements-dev.txt b/requirements-dev.txt index adf5a81c3..4fbf21260 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,14 +4,14 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.9.1 +flake8==3.9.2 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.2.1 +flake8-tidy-imports==4.3.0 mypy==0.812 -pytest==6.2.3 +pytest==6.2.4 pytest-asyncio==0.15.1 -pytest-cov==2.11.1 -pytest-mock==3.6.0 +pytest-cov==2.12.0 +pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.8.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 79ad722e2..5e7e9d9d2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.6.3 -scikit-learn==0.24.1 +scikit-learn==0.24.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.1 diff --git a/requirements.txt b/requirements.txt index 94f1739a1..512b2a588 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,23 @@ -numpy==1.20.2 +numpy==1.20.3 pandas==1.2.4 -ccxt==1.48.76 +ccxt==1.50.48 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.11 -python-telegram-bot==13.4.1 -arrow==1.0.3 -cachetools==4.2.1 +SQLAlchemy==1.4.15 +python-telegram-bot==13.5 +arrow==1.1.0 +cachetools==4.2.2 requests==2.25.1 urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 -TA-Lib==0.4.19 -technical==1.2.2 +TA-Lib==0.4.20 +technical==1.3.0 tabulate==0.8.9 pycoingecko==2.0.0 -jinja2==2.11.3 +jinja2==3.0.1 tables==3.6.1 blosc==1.10.2 @@ -31,10 +31,10 @@ python-rapidjson==1.0 sdnotify==0.3.2 # API Server -fastapi==0.63.0 +fastapi==0.65.1 uvicorn==0.13.4 -pyjwt==2.0.1 -aiofiles==0.6.0 +pyjwt==2.1.0 +aiofiles==0.7.0 # Support for colorized terminal output colorama==0.4.4 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 900b784f2..ece0a253e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -396,7 +396,7 @@ def main(args): sys.exit() config = load_config(args['config']) - url = config.get('api_server', {}).get('server_url', '127.0.0.1') + url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1') port = config.get('api_server', {}).get('listen_port', '8080') username = config.get('api_server', {}).get('username') password = config.get('api_server', {}).get('password') diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d86bced5d..4d3937d87 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,242 +918,244 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, + saved_hyperopt_results_legacy): + for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', + MagicMock(return_value=saved_hyperopt_results_legacy) + ) + + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", + " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--best", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 5/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", + "Sell hyperspace params", "ROI table", "Stoploss"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--max-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-profit", "0.11", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-profit", "0.10", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-time", "2000", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", + " 8/12", " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-time", "1500", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 6/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" + " 9/12", " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--export-csv", "test_file.csv", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + log_has("CSV file created: test_file.csv", caplog) + f = Path("test_file.csv") + assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() + assert f.is_file() + f.unlink() + + +def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) - ) - - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", - " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--best", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 5/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", - "Sell hyperspace params", "ROI table", "Stoploss"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--max-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-profit", "0.11", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-profit", "0.10", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--max-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-time", "2000", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", - " 8/12", " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-time", "1500", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 6/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" - " 9/12", " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--export-csv", "test_file.csv", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - log_has("CSV file created: test_file.csv", caplog) - f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() - assert f.is_file() - f.unlink() - - -def test_hyperopt_show(mocker, capsys, hyperopt_results): - mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) + MagicMock(return_value=saved_hyperopt_results) ) args = [ diff --git a/tests/conftest.py b/tests/conftest.py index 788586134..ef2bd0613 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json import logging import re from copy import deepcopy -from datetime import datetime +from datetime import datetime, timedelta from functools import reduce from pathlib import Path from unittest.mock import MagicMock, Mock, PropertyMock @@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6) @@ -1677,6 +1678,7 @@ def buy_order_fee(): @pytest.fixture(scope="function") def edge_conf(default_conf): conf = deepcopy(default_conf) + conf['runmode'] = RunMode.DRY_RUN conf['max_open_trades'] = -1 conf['tradable_balance_ratio'] = 0.5 conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT @@ -1778,7 +1780,7 @@ def open_trade(): @pytest.fixture -def hyperopt_results(): +def saved_hyperopt_results_legacy(): return [ { 'loss': 0.4366182531161519, @@ -1907,3 +1909,136 @@ def hyperopt_results(): 'is_best': False } ] + + +@pytest.fixture +def saved_hyperopt_results(): + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + # New Hyperopt mode! + 'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 6b33fa7f2..b87258c64 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -246,3 +246,46 @@ def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history): assert dataframe.empty assert isinstance(time, datetime) assert time == datetime(1970, 1, 1, tzinfo=timezone.utc) + + # Test backtest mode + default_conf["runmode"] = RunMode.BACKTEST + dp._set_dataframe_max_index(1) + dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe) + + assert len(dataframe) == 1 + + dp._set_dataframe_max_index(2) + dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe) + assert len(dataframe) == 2 + + dp._set_dataframe_max_index(3) + dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe) + assert len(dataframe) == 3 + + dp._set_dataframe_max_index(500) + dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe) + assert len(dataframe) == len(ohlcv_history) + + +def test_no_exchange_mode(default_conf): + dp = DataProvider(default_conf, None) + + message = "Exchange is not available to DataProvider." + + with pytest.raises(OperationalException, match=message): + dp.refresh([()]) + + with pytest.raises(OperationalException, match=message): + dp.ohlcv('XRP/USDT', '5m') + + with pytest.raises(OperationalException, match=message): + dp.market('XRP/USDT') + + with pytest.raises(OperationalException, match=message): + dp.ticker('XRP/USDT') + + with pytest.raises(OperationalException, match=message): + dp.orderbook('XRP/USDT', 20) + + with pytest.raises(OperationalException, match=message): + dp.available_pairs() diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 25e0da5e2..c4620e1c7 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -344,6 +344,8 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): def test_edge_process_no_pairs(mocker, edge_conf, caplog): edge_conf['exchange']['pair_whitelist'] = [] + mocker.patch('freqtrade.freqtradebot.validate_config_consistency') + freqtrade = get_patched_freqtradebot(mocker, edge_conf) fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 573f41bda..b6b395802 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2084,6 +2084,46 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123}) + mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', return_value={'for': 123}) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + return_value={'fee': {}, 'status': 'canceled', 'amount': 1234}) + mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', + return_value={'fee': {}, 'status': 'canceled', 'amount': 1234}) + co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) + assert co == {'fee': {}, 'status': 'canceled', 'amount': 1234} + + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + return_value='canceled') + mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', + return_value='canceled') + # Fall back to fetch_stoploss_order + co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) + assert co == {'for': 123} + + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', + side_effect=InvalidOrderException("")) + mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', + side_effect=InvalidOrderException("")) + + co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) + assert co['amount'] == 555 + assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}} + + with pytest.raises(InvalidOrderException): + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + side_effect=InvalidOrderException("Did not find order")) + mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', + side_effect=InvalidOrderException("Did not find order")) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 494d86e56..63d99acdf 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -157,3 +157,26 @@ def test_fetch_stoploss_order(default_conf, mocker): 'fetch_stoploss_order', 'fetch_orders', retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') + + +def test_get_order_id(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='ftx') + order = { + 'type': STOPLOSS_ORDERTYPE, + 'price': 1500, + 'id': '1111', + 'info': { + 'orderId': '1234' + } + } + assert exchange.get_order_id_conditional(order) == '1234' + + order = { + 'type': 'limit', + 'price': 1500, + 'id': '1111', + 'info': { + 'orderId': '1234' + } + } + assert exchange.get_order_id_conditional(order) == '1111' diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 97f428e2f..ed22cde92 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -90,6 +90,7 @@ def test_get_balances_prod(default_conf, mocker): '3ST': balance_item.copy(), '4ST': balance_item.copy(), 'EUR': balance_item.copy(), + 'timestamp': 123123 }) kraken_open_orders = [{'symbol': '1ST/EUR', 'type': 'limit', @@ -138,7 +139,7 @@ def test_get_balances_prod(default_conf, mocker): default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") balances = exchange.get_balances() - assert len(balances) == 5 + assert len(balances) == 6 assert balances['1ST']['free'] == 9.0 assert balances['1ST']['total'] == 10.0 diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3655b941d..c35a083ec 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -185,7 +185,7 @@ tc11 = BTContainer(data=[ [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], - [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [3, 5000, 5150, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, @@ -440,6 +440,23 @@ tc27 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] ) +# Test 28: trailing_stop should raise so candle 3 causes a stoploss +# Same case than tc11 - but candle 3 "gaps down" - the stoploss will be above the candle, +# therefore "open" will be used +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 +tc28 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, tc1, @@ -469,6 +486,7 @@ TESTS = [ tc25, tc26, tc27, + tc28, ] @@ -493,6 +511,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.advise_buy = lambda a, m: frame backtesting.strategy.advise_sell = lambda a, m: frame caplog.set_level(logging.DEBUG) @@ -501,13 +520,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: # Dummy data as we mock the analyze functions data_processed = {pair: frame.copy()} min_date, max_date = get_timerange({pair: frame}) - results = backtesting.backtest( + result = backtesting.backtest( processed=data_processed, start_date=min_date, end_date=max_date, max_open_trades=10, ) + results = result['results'] assert len(results) == len(data.trades) assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 00114be5b..632d458ce 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -83,6 +83,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None: patch_exchange(mocker) config['timeframe'] = '1m' backtesting = Backtesting(config) + backtesting._set_strategy(backtesting.strategylist[0]) data = load_data_test(contour, testdatadir) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) @@ -106,6 +107,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) + backtesting._set_strategy(backtesting.strategylist[0]) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) return { @@ -285,6 +287,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: patch_exchange(mocker) get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.config == default_conf assert backtesting.timeframe == '5m' assert callable(backtesting.strategy.ohlcvdata_to_dataframe) @@ -315,11 +318,13 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None: fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.fee == 0.1234 assert fee_mock.call_count == 0 default_conf['fee'] = 0.0 backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.fee == 0.0 assert fee_mock.call_count == 0 @@ -330,6 +335,7 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) assert len(processed['UNITTEST/BTC']) == 102 @@ -361,12 +367,13 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: default_conf['timerange'] = '-1510694220' backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.bot_loop_start = MagicMock() backtesting.start() # check the logs, that will contain the backtest result exists = [ 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:59:00 (0 days)..' + 'up to 2017-11-14 22:59:00 (0 days).' ] for line in exists: assert log_has(line, caplog) @@ -393,6 +400,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> default_conf['timerange'] = '20180101-20180102' backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) with pytest.raises(OperationalException, match='No data found. Terminating.'): backtesting.start() @@ -465,6 +473,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: default_conf['stake_amount'] = 'unlimited' default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) pair = 'UNITTEST/BTC' row = [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), @@ -508,19 +517,21 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) pair = 'UNITTEST/BTC' timerange = TimeRange('date', None, 1517227800, 0) data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) - results = backtesting.backtest( + result = backtesting.backtest( processed=processed, start_date=min_date, end_date=max_date, max_open_trades=10, position_stacking=False, ) + results = result['results'] assert not results.empty assert len(results) == 2 @@ -569,6 +580,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) # Run a backtesting for an exiting 1min timeframe timerange = TimeRange.parse_timerange('1510688220-1510700340') @@ -583,13 +595,14 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None max_open_trades=1, position_stacking=False, ) - assert not results.empty - assert len(results) == 1 + assert not results['results'].empty + assert len(results['results']) == 1 def test_processed(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) dict_of_tickerrows = load_data_test('raise', testdatadir) dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows) @@ -623,7 +636,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres @pytest.mark.parametrize('protections,contour,expected', [ @@ -648,7 +661,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == expected + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): @@ -660,10 +673,11 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_only_sell(mocker, default_conf, testdatadir): @@ -675,10 +689,11 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): @@ -688,12 +703,14 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): pair='UNITTEST/BTC', datadir=testdatadir) default_conf['timeframe'] = '1m' backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override - results = backtesting.backtest(**backtest_conf) + result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) # 100 buys signals + results = result['results'] assert len(results) == 100 # One trade was force-closed at the end assert len(results.loc[results['is_open']]) == 0 @@ -729,6 +746,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) default_conf['timeframe'] = '5m' backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_sell = _trend_alternate_hold # Override @@ -745,9 +763,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, '5m', 2)) > 0 + assert len(evaluate_result_multi(results['results'], '5m', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades - assert len(evaluate_result_multi(results, '5m', 3)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 backtest_conf = { 'processed': processed, @@ -757,7 +775,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) 'position_stacking': False, } results = backtesting.backtest(**backtest_conf) - assert len(evaluate_result_multi(results, '5m', 1)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0 def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @@ -789,9 +807,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...' ] @@ -802,8 +820,20 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) - backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS)) + backtestmock = MagicMock(return_value={ + 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), + 'config': default_conf, + 'locks': [], + 'rejected_signals': 20, + 'final_balance': 1000, + }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) @@ -817,7 +847,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): text_table_strategy=strattable_mock, generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, - generate_strategy_metrics=strat_summary, + generate_strategy_comparison=strat_summary, generate_daily_stats=MagicMock(), ) patched_configuration_load_config_file(mocker, default_conf) @@ -851,9 +881,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', @@ -865,41 +895,60 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): - + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) + result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'trade_duration': [235, 40], + 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_ratio': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'trade_duration': [47, 40, 20], + 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }) backtestmock = MagicMock(side_effect=[ - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], - 'profit_ratio': [0.0, 0.0], - 'profit_abs': [0.0, 0.0], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', ], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', ], utc=True), - 'trade_duration': [235, 40], - 'is_open': [False, False], - 'stake_amount': [0.01, 0.01], - 'open_rate': [0.104445, 0.10302485], - 'close_rate': [0.104969, 0.103541], - 'sell_reason': [SellType.ROI, SellType.ROI] - }), - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], - 'profit_ratio': [0.03, 0.01, 0.1], - 'profit_abs': [0.01, 0.02, 0.2], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', - '2018-01-30 05:30:00'], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', - '2018-01-30 08:30:00'], utc=True), - 'trade_duration': [47, 40, 20], - 'is_open': [False, False, False], - 'stake_amount': [0.01, 0.01, 0.01], - 'open_rate': [0.104445, 0.10302485, 0.122541], - 'close_rate': [0.104969, 0.103541, 0.123541], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - }), + { + 'results': result1, + 'config': default_conf, + 'locks': [], + 'rejected_signals': 20, + 'final_balance': 1000, + }, + { + 'results': result2, + 'config': default_conf, + 'locks': [], + 'rejected_signals': 20, + 'final_balance': 1000, + } ]) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) @@ -930,9 +979,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 7cb04fae0..07d208210 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -4,7 +4,7 @@ import re from datetime import datetime from pathlib import Path from typing import Dict, List -from unittest.mock import MagicMock +from unittest.mock import ANY, MagicMock import pandas as pd import pytest @@ -17,10 +17,12 @@ from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.hyper import IntParameter +from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -28,23 +30,7 @@ from .hyperopts.default_hyperopt import DefaultHyperOpt # Functions for recurrent object patching -def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: - """ - When creating results, mock the hyperopt so that *by default* - - we don't create any pickle'd files in the filesystem - - we might have a pickle'd file so make sure that we return - false when looking for it - """ - hyperopt.results_file = testdatadir / 'optimize/ut_results.pickle' - - mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) - stat_mock = MagicMock() - stat_mock.st_size = 1 - mocker.patch.object(Path, "stat", MagicMock(return_value=stat_mock)) - - mocker.patch.object(Path, "unlink", MagicMock(return_value=True)) - mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) - mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') +def create_results() -> List[Dict]: return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}] @@ -318,54 +304,49 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: assert caplog.record_tuples == [] -def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None: - epochs = create_results(mocker, hyperopt, testdatadir) - mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) - mock_dump_json = mocker.patch('freqtrade.optimize.hyperopt.file_dump_json', return_value=None) - results_file = testdatadir / 'optimize' / 'ut_results.pickle' +def test_save_results_saves_epochs(mocker, hyperopt, tmpdir, caplog) -> None: + # Test writing to temp dir and reading again + epochs = create_results() + hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt') caplog.set_level(logging.DEBUG) - hyperopt.epochs = epochs - hyperopt._save_results() - assert log_has(f"1 epoch saved to '{results_file}'.", caplog) - mock_dump.assert_called_once() - mock_dump_json.assert_called_once() + for epoch in epochs: + hyperopt._save_result(epoch) + assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog) - hyperopt.epochs = epochs + epochs - hyperopt._save_results() - assert log_has(f"2 epochs saved to '{results_file}'.", caplog) + hyperopt._save_result(epochs[0]) + assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog) + + hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file) + assert len(hyperopt_epochs) == 2 -def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: - epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) - results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = HyperoptTools._read_results(results_file) - assert log_has(f"Reading epochs from '{results_file}'", caplog) - assert hyperopt_epochs == epochs - mock_load.assert_called_once() +def test_load_previous_results(testdatadir, caplog) -> None: - -def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: - epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) - mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) - statmock = MagicMock() - statmock.st_size = 5 - # mocker.patch.object(Path, 'stat', MagicMock(return_value=statmock)) - - results_file = testdatadir / 'optimize' / 'ut_results.pickle' + results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' hyperopt_epochs = HyperoptTools.load_previous_results(results_file) - assert hyperopt_epochs == epochs - mock_load.assert_called_once() + assert len(hyperopt_epochs) == 5 + assert log_has_re(r"Reading pickled epochs from .*", caplog) - del epochs[0]['is_best'] - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + caplog.clear() - with pytest.raises(OperationalException): + # Modern version + results_file = testdatadir / 'strategy_SampleStrategy.fthypt' + + hyperopt_epochs = HyperoptTools.load_previous_results(results_file) + + assert len(hyperopt_epochs) == 5 + assert log_has_re(r"Reading epochs from .*", caplog) + + +def test_load_previous_results2(mocker, testdatadir, caplog) -> None: + mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle', + return_value=[{'asdf': '222'}]) + results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' + with pytest.raises(OperationalException, match=r"The file .* incompatible.*"): HyperoptTools.load_previous_results(results_file) @@ -383,7 +364,8 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', @@ -422,9 +404,9 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out - assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + # Should be called for historical candle data + assert dumper.call_count == 1 + assert dumper2.call_count == 1 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") @@ -432,18 +414,42 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: assert hasattr(hyperopt, "position_stacking") -def test_format_results(hyperopt): - # Test with BTC as stake_currency - trades = [ - ('ETH/BTC', 2, 2, 123), - ('LTC/BTC', 1, 1, 123), - ('XPR/BTC', -1, -2, -246) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - df = pd.DataFrame.from_records(trades, columns=labels) - results_metrics = hyperopt._calculate_results_metrics(df) - results_explanation = hyperopt._format_results_explanation_string(results_metrics) - total_profit = results_metrics['total_profit'] +def test_hyperopt_format_results(hyperopt): + + bt_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt.config, + 'locks': [], + 'final_balance': 0.02, + 'rejected_signals': 2, + 'backtest_start_time': 1619718665, + 'backtest_end_time': 1619718665, + } + results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, + Arrow(2017, 11, 14, 19, 32, 00), + Arrow(2017, 12, 14, 19, 32, 00), market_change=0) + + results_explanation = HyperoptTools.format_results_explanation_string(results_metrics, 'BTC') + total_profit = results_metrics['profit_total_abs'] results = { 'loss': 0.0, @@ -457,21 +463,9 @@ def test_format_results(hyperopt): } result = HyperoptTools._format_explanation_string(results, 1) - assert result.find(' 66.67%') - assert result.find('Total profit 1.00000000 BTC') - assert result.find('2.0000Σ %') - - # Test with EUR as stake_currency - trades = [ - ('ETH/EUR', 2, 2, 123), - ('LTC/EUR', 1, 1, 123), - ('XPR/EUR', -1, -2, -246) - ] - df = pd.DataFrame.from_records(trades, columns=labels) - results_metrics = hyperopt._calculate_results_metrics(df) - results['total_profit'] = results_metrics['total_profit'] - result = HyperoptTools._format_explanation_string(results, 1) - assert result.find('Total profit 1.00000000 EUR') + assert ' 0.71%' in result + assert 'Total profit 0.00003100 BTC' in result + assert '0:50:00 min' in result @pytest.mark.parametrize("spaces, expected_results", [ @@ -502,10 +496,10 @@ def test_format_results(hyperopt): (['default', 'buy'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), ]) -def test_has_space(hyperopt, spaces, expected_results): +def test_has_space(hyperopt_conf, spaces, expected_results): for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - hyperopt.config.update({'spaces': spaces}) - assert hyperopt.has_space(s) == expected_results[s] + hyperopt_conf.update({'spaces': spaces}) + assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s] def test_populate_indicators(hyperopt, testdatadir) -> None: @@ -576,22 +570,39 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'hyperopt_min_trades': 1, }) - trades = [ - ('TRX/BTC', 0.023117, 0.000233, 100) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - backtest_result = pd.DataFrame.from_records(trades, columns=labels) + backtest_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt_conf, + 'locks': [], + 'rejected_signals': 20, + 'final_balance': 1000, + } - mocker.patch( - 'freqtrade.optimize.hyperopt.Backtesting.backtest', - MagicMock(return_value=backtest_result) - ) - mocker.patch( - 'freqtrade.optimize.hyperopt.get_timerange', - MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) - ) + mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) + mocker.patch('freqtrade.optimize.hyperopt.get_timerange', + return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) - mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) + mocker.patch.object(Path, 'open') + mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { 'adx-value': 0, @@ -625,11 +636,11 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_only_offset_is_reached': False, } response_expected = { - 'loss': 1.9840569076926293, - 'results_explanation': (' 1 trades. 1/0/0 Wins/Draws/Losses. ' - 'Avg profit 2.31%. Median profit 2.31%. Total profit ' - '0.00023300 BTC ( 2.31%). ' - 'Avg duration 100.0 min.' + 'loss': 1.9147239021396234, + 'results_explanation': (' 4 trades. 4/0/0 Wins/Draws/Losses. ' + 'Avg profit 0.77%. Median profit 0.71%. Total profit ' + '0.00003100 BTC ( 0.00%). ' + 'Avg duration 0:50:00 min.' ), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, @@ -640,10 +651,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'rsi-enabled': False, 'rsi-value': 0, 'trigger': 'macd_cross_signal'}, - 'roi': {0: 0.12000000000000001, - 20.0: 0.02, - 50.0: 0.01, - 110.0: 0}, + 'roi': {"0": 0.12000000000000001, + "20.0": 0.02, + "50.0": 0.01, + "110.0": 0}, 'sell': {'sell-adx-enabled': False, 'sell-adx-value': 0, 'sell-fastd-enabled': True, @@ -659,21 +670,16 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, - 'results_metrics': {'avg_profit': 2.3117, - 'draws': 0, - 'duration': 100.0, - 'losses': 0, - 'winsdrawslosses': ' 1 0 0', - 'median_profit': 2.3117, - 'profit': 2.3117, - 'total_profit': 0.000233, - 'trade_count': 1, - 'wins': 1}, - 'total_profit': 0.00023300 + 'params_not_optimized': {'buy': {}, 'sell': {}}, + 'results_metrics': ANY, + 'total_profit': 3.1e-08 } hyperopt = Hyperopt(hyperopt_conf) - hyperopt.dimensions = hyperopt.hyperopt_space() + hyperopt.min_date = Arrow(2017, 12, 10) + hyperopt.max_date = Arrow(2017, 12, 13) + hyperopt.init_spaces() + hyperopt.dimensions = hyperopt.dimensions generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected @@ -690,7 +696,8 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', @@ -741,13 +748,14 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: ':{},"stoploss":null,"trailing_stop":null}' ) assert result_str in out # noqa: E501 - assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + # Should be called for historical candle data + assert dumper.call_count == 1 + assert dumper2.call_count == 1 def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -789,13 +797,14 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: out, err = capsys.readouterr() assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 - assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + # Should be called for historical candle data + assert dumper.call_count == 1 + assert dumper2.call_count == 1 def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -836,13 +845,14 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: out, err = capsys.readouterr() assert '{"minimal_roi":{},"stoploss":null}' in out - assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + + assert dumper.call_count == 1 + assert dumper2.call_count == 1 def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -884,9 +894,9 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out - assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + assert dumper.call_count == 1 + assert dumper2.call_count == 1 + assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") @@ -922,7 +932,8 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -965,8 +976,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + assert dumper.call_count == 1 + assert dumper2.call_count == 1 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") @@ -975,7 +986,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') + dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -1018,8 +1030,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called - # Should be called twice, once for historical candle data, once to save evaluations - assert dumper.call_count == 2 + assert dumper.call_count == 1 + assert dumper2.call_count == 1 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") @@ -1107,7 +1119,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True + assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) @@ -1132,3 +1144,17 @@ def test_SKDecimal(): assert space.transform([2.0]) == [200] assert space.transform([1.0]) == [100] assert space.transform([1.5, 1.6]) == [150, 160] + + +def test___pprint(): + params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'} + non_params = {'buy_notoptimied': 55} + + x = HyperoptTools._pprint(params, non_params) + assert x == """{ + "buy_std": 1.2, + "buy_rsi": 31, + "buy_enable": True, + "buy_what": "asdf", + "buy_notoptimied": 55, # value loaded from strategy +}""" diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8119c732b..f9dac3397 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,5 +1,6 @@ +import datetime import re -from datetime import datetime, timedelta, timezone +from datetime import timedelta from pathlib import Path import pandas as pd @@ -7,14 +8,15 @@ import pytest from arrow import Arrow from freqtrade.configuration import TimeRange -from freqtrade.constants import LAST_BT_RESULT_FN +from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_metrics, store_backtest_stats, + generate_strategy_comparison, + generate_trading_stats, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -26,25 +28,22 @@ def test_text_table_bt_results(): results = pd.DataFrame( { - 'pair': ['ETH/BTC', 'ETH/BTC'], - 'profit_ratio': [0.1, 0.2], - 'profit_abs': [0.2, 0.4], - 'trade_duration': [10, 30], - 'wins': [2, 0], - 'draws': [0, 0], - 'losses': [0, 0] + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_ratio': [0.1, 0.2, -0.05], + 'profit_abs': [0.2, 0.4, -0.1], + 'trade_duration': [10, 30, 20], } ) result_str = ( - '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' - ' Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' - '|---------+--------+----------------+----------------+------------------+' - '----------------+----------------+--------+---------+----------|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 |' - ' 15.00 | 0:20:00 | 2 | 0 | 0 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' - ' 15.00 | 0:20:00 | 2 | 0 | 0 |' + '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |' + ' Avg Duration | Win Draw Loss Win% |\n' + '|---------+--------+----------------+----------------+------------------+----------------+' + '----------------+-------------------------|\n' + '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' + ' 0:20:00 | 2 0 1 66.7 |\n' + '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' + ' 0:20:00 | 2 0 1 66.7 |' ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', @@ -80,6 +79,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -96,8 +96,8 @@ def test_generate_backtest_stats(default_conf, testdatadir): assert 'DefStrat' in stats['strategy'] assert 'strategy_comparison' in stats strat_stats = stats['strategy']['DefStrat'] - assert strat_stats['backtest_start'] == min_date.datetime - assert strat_stats['backtest_end'] == max_date.datetime + assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT) + assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT) assert strat_stats['total_trades'] == len(results['DefStrat']['results']) # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 @@ -127,6 +127,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -140,8 +141,8 @@ def test_generate_backtest_stats(default_conf, testdatadir): strat_stats = stats['strategy']['DefStrat'] assert strat_stats['max_drawdown'] == 0.013803 - assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc) - assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc) + assert strat_stats['drawdown_start'] == '2017-11-14 22:10:00' + assert strat_stats['drawdown_end'] == '2017-11-14 22:43:00' assert strat_stats['drawdown_end_ts'] == 1510699380000 assert strat_stats['drawdown_start_ts'] == 1510697400000 assert strat_stats['pairlist'] == ['UNITTEST/BTC'] @@ -226,8 +227,6 @@ def test_generate_daily_stats(testdatadir): assert res['winning_days'] == 14 assert res['draw_days'] == 4 assert res['losing_days'] == 3 - assert res['winner_holding_avg'] == timedelta(seconds=1440) - assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) # Select empty dataframe! res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) @@ -238,6 +237,23 @@ def test_generate_daily_stats(testdatadir): assert res['losing_days'] == 0 +def test_generate_trading_stats(testdatadir): + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_data(filename) + res = generate_trading_stats(bt_data) + assert isinstance(res, dict) + assert res['winner_holding_avg'] == timedelta(seconds=1440) + assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) + assert 'wins' in res + assert 'losses' in res + assert 'draws' in res + + # Select empty dataframe! + res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) + assert res['wins'] == 0 + assert res['losses'] == 0 + + def test_text_table_sell_reason(): results = pd.DataFrame( @@ -254,14 +270,14 @@ def test_text_table_sell_reason(): ) result_str = ( - '| Sell Reason | Sells | Wins | Draws | Losses |' - ' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n' - '|---------------+---------+--------+---------+----------+' - '----------------+----------------+------------------+----------------|\n' - '| roi | 2 | 2 | 0 | 0 |' - ' 15 | 30 | 0.6 | 15 |\n' - '| stop_loss | 1 | 0 | 0 | 1 |' - ' -10 | -10 | -0.2 | -5 |' + '| Sell Reason | Sells | Win Draws Loss Win% | Avg Profit % | Cum Profit % |' + ' Tot Profit BTC | Tot Profit % |\n' + '|---------------+---------+--------------------------+----------------+----------------+' + '------------------+----------------|\n' + '| roi | 2 | 2 0 0 100 | 15 | 30 |' + ' 0.6 | 15 |\n' + '| stop_loss | 1 | 0 0 1 0 | -10 | -10 |' + ' -0.2 | -5 |' ) sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, @@ -309,9 +325,12 @@ def test_text_table_strategy(default_conf): default_conf['max_open_trades'] = 2 default_conf['dry_run_wallet'] = 3 results = {} + date = datetime.datetime(year=2020, month=1, day=1, hour=12, minute=30) + delta = datetime.timedelta(days=1) results['TestStrategy1'] = {'results': pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'close_date': [date, date + delta, date + delta * 2], 'profit_ratio': [0.1, 0.2, 0.3], 'profit_abs': [0.2, 0.4, 0.5], 'trade_duration': [10, 30, 10], @@ -324,6 +343,7 @@ def test_text_table_strategy(default_conf): results['TestStrategy2'] = {'results': pd.DataFrame( { 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], + 'close_date': [date, date + delta, date + delta * 2], 'profit_ratio': [0.4, 0.2, 0.3], 'profit_abs': [0.4, 0.4, 0.5], 'trade_duration': [15, 30, 15], @@ -335,18 +355,17 @@ def test_text_table_strategy(default_conf): ), 'config': default_conf} result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot' - ' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' + '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' + ' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' '|---------------+--------+----------------+----------------+------------------+' - '----------------+----------------+--------+---------+----------|\n' + '----------------+----------------+-------------------------+-----------------------|\n' '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' - ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n' + ' 36.67 | 0:17:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |\n' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' - ' 43.33 | 0:20:00 | 3 | 0 | 0 |' + ' 43.33 | 0:20:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |' ) - strategy_results = generate_strategy_metrics(all_results=results) - + strategy_results = generate_strategy_comparison(all_results=results) assert text_table_strategy(strategy_results, 'BTC') == result_str diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 8b060c287..5e2274ce3 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -7,10 +7,11 @@ import pytest from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException +from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_freqtradebot, log_has, log_has_re +from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re @pytest.fixture(scope="function") @@ -406,6 +407,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), + ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "max_value": 0.000001}], + "USDT", ['NANO/USDT']), ([{"method": "StaticPairList"}, {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], @@ -488,6 +492,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t r'because last price < .*%$', caplog) or log_has_re(r'^Removed .* from whitelist, ' r'because last price > .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because min value change of .*', caplog) or log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", caplog)) if pairlist['method'] == 'VolumePairList': @@ -512,6 +518,18 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: PairListManager(MagicMock, whitelist_conf) +def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] + if hasattr(Trade, 'query'): + del Trade.query + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + exchange = get_patched_exchange(mocker, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf) + pm.refresh_pairlist() + + assert log_has("PerformanceFilter is not available in this mode.", caplog) + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] @@ -787,6 +805,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", None ), + ({"method": "PriceFilter", "max_value": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]", + None + ), ({"method": "PriceFilter"}, "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", None @@ -803,6 +825,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected + ({"method": "PriceFilter", "max_value": -1.00010000}, + None, + "PriceFilter requires max_value to be >= 0" + ), # OperationalException expected ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " "0.01 over the last days.'}]", diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 2d43addff..5174f9416 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 +import datetime from unittest.mock import MagicMock import pytest @@ -21,6 +22,12 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._backoff = 0 + mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', + return_value=None) + assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 0.0 + with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC') @@ -115,6 +122,28 @@ def test_fiat_convert_without_network(mocker): CryptoToFiatConverter._coingekko = cmc_temp +def test_fiat_too_many_requests_response(mocker, caplog): + # Because CryptoToFiatConverter is a Singleton we reset the listings + req_exception = "429 Too Many Requests" + listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception)) + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_coins_list=listmock, + ) + # with pytest.raises(RequestEsxception): + fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._load_cryptomap() + + length_cryptomap = len(fiat_convert._cryptomap) + assert length_cryptomap == 0 + assert fiat_convert._backoff > datetime.datetime.now().timestamp() + assert log_has( + 'Too many requests for Coingecko API, backing off and trying again later.', + caplog + ) + + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6d31e7635..e7a968e37 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -199,28 +199,31 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = 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' in result[0][1] assert '-0.41%' == result[0][3] + assert isnan(fiat_profit_sum) # Test with fiatconvert rpc._fiat_converter = CryptoToFiatConverter() - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = 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' in result[0][1] assert '-0.41% (-0.06)' == result[0][3] + assert '-0.06' == f'{fiat_profit_sum:.2f}' mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert 'nan%' == result[0][3] + assert isnan(fiat_profit_sum) def test_rpc_daily_profit(default_conf, update, ticker, fee, @@ -419,7 +422,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now' - assert stats['avg_duration'] == '0:00:00' + assert stats['avg_duration'] in ('0:00:00', '0:00:01') assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) @@ -430,7 +433,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now' - assert stats['avg_duration'] == '0:00:00' + assert stats['avg_duration'] in ('0:00:00', '0:00:01') assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) assert isnan(stats['profit_all_coin']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 69d312e65..1a66b2e81 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -710,7 +710,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,): assert 'draws' in rc.json()['durations'] -def test_api_performance(botclient, mocker, ticker, fee): +def test_api_performance(botclient, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) @@ -728,6 +728,7 @@ def test_api_performance(botclient, mocker, ticker, fee): ) trade.close_profit = trade.calc_profit_ratio() + trade.close_profit_abs = trade.calc_profit() Trade.query.session.add(trade) trade = Trade( @@ -743,14 +744,16 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.391 ) trade.close_profit = trade.calc_profit_ratio() + trade.close_profit_abs = trade.calc_profit() + Trade.query.session.add(trade) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json()) == 2 - assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61}, - {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}] + assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279}, + {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}] def test_api_status(botclient, mocker, ticker, fee, markets): @@ -1142,6 +1145,14 @@ def test_api_plot_config(botclient): assert_response(rc) assert rc.json() == ftbot.strategy.plot_config assert isinstance(rc.json()['main_plot'], dict) + assert isinstance(rc.json()['subplots'], dict) + + ftbot.strategy.plot_config = {'main_plot': {'sma': {}}} + rc = client_get(client, f"{BASE_URI}/plot_config") + assert_response(rc) + + assert isinstance(rc.json()['main_plot'], dict) + assert isinstance(rc.json()['subplots'], dict) def test_api_strategies(botclient): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6a36c12a7..e640f2dff 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -4,6 +4,7 @@ import re from datetime import datetime +from functools import reduce from random import choice, randint from string import ascii_uppercase from unittest.mock import ANY, MagicMock @@ -54,6 +55,14 @@ class DummyCls(Telegram): raise Exception('test') +def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None): + inline_msg_mock = MagicMock() + telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf) + mocker.patch('freqtrade.rpc.telegram.Telegram._send_inline_msg', inline_msg_mock) + + return telegram, ftbot, msg_mock, inline_msg_mock + + def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() if mock: @@ -902,6 +911,33 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' +def test_forcebuy_no_pair(default_conf, update, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + + fbuy_mock = MagicMock(return_value=None) + mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) + + telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, + default_conf) + patch_get_signal(freqtradebot, (True, False)) + + context = MagicMock() + context.args = [] + telegram._forcebuy(update=update, context=context) + + assert fbuy_mock.call_count == 0 + assert inline_msg_mock.call_count == 1 + assert inline_msg_mock.call_args_list[0][0][0] == 'Which pair?' + assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' + keyboard = inline_msg_mock.call_args_list[0][1]['keyboard'] + assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 + update = MagicMock() + update.callback_query = MagicMock() + update.callback_query.data = 'XRP/USDT' + telegram._forcebuy_inline(update, None) + assert fbuy_mock.call_count == 1 + + def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: @@ -929,7 +965,7 @@ def test_performance_handle(default_conf, update, ticker, fee, telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'ETH/BTC\t6.20% (1)' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: @@ -969,6 +1005,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) + telegram._locks(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'No active locks.' in msg_mock.call_args_list[0][0][0] + + msg_mock.reset_mock() PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') @@ -1102,6 +1143,15 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
 
+    msg_mock.reset_mock()
+
+    mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
+        return_value={}))
+    telegram._edge(update=update, context=MagicMock())
+    assert msg_mock.call_count == 1
+    assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
+    assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
+
 
 def test_telegram_trades(mocker, update, default_conf, fee):
 
diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py
index a8862e9c9..92ac9f63a 100644
--- a/tests/strategy/test_default_strategy.py
+++ b/tests/strategy/test_default_strategy.py
@@ -36,10 +36,11 @@ def test_default_strategy(result, fee):
     )
 
     assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
-                                        rate=20000, time_in_force='gtc') is True
+                                        rate=20000, time_in_force='gtc',
+                                        current_time=datetime.utcnow()) is True
     assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
-                                       rate=20000, time_in_force='gtc', sell_reason='roi') is True
+                                       rate=20000, time_in_force='gtc', sell_reason='roi',
+                                       current_time=datetime.utcnow()) is True
 
     assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
-                                    current_rate=20_000, current_profit=0.05, dataframe=None
-                                    ) == strategy.stoploss
+                                    current_rate=20_000, current_profit=0.05) == strategy.stoploss
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
index bd81bc80c..ded396779 100644
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
     now = arrow.utcnow().datetime
     sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade,
                                          current_time=now, current_profit=profit,
-                                         force_stoploss=0, high=None, dataframe=None)
+                                         force_stoploss=0, high=None)
     assert isinstance(sl_flag, SellCheckTuple)
     assert sl_flag.sell_type == expected
     if expected == SellType.NONE:
@@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
 
     sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade,
                                          current_time=now, current_profit=profit2,
-                                         force_stoploss=0, high=None, dataframe=None)
+                                         force_stoploss=0, high=None)
     assert sl_flag.sell_type == expected2
     if expected2 == SellType.NONE:
         assert sl_flag.sell_flag is False
@@ -399,27 +399,27 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
     )
 
     now = arrow.utcnow().datetime
-    res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
+    res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
 
     assert res.sell_flag is False
     assert res.sell_type == SellType.NONE
 
     strategy.custom_sell = MagicMock(return_value=True)
-    res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
+    res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
     assert res.sell_flag is True
     assert res.sell_type == SellType.CUSTOM_SELL
     assert res.sell_reason == 'custom_sell'
 
     strategy.custom_sell = MagicMock(return_value='hello world')
 
-    res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
+    res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
     assert res.sell_type == SellType.CUSTOM_SELL
     assert res.sell_flag is True
     assert res.sell_reason == 'hello world'
 
     caplog.clear()
     strategy.custom_sell = MagicMock(return_value='h' * 100)
-    res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
+    res = strategy.should_sell(trade, 1, now, False, False, None, None, 0)
     assert res.sell_type == SellType.CUSTOM_SELL
     assert res.sell_flag is True
     assert res.sell_reason == 'h' * 64
@@ -636,7 +636,7 @@ def test_hyperopt_parameters():
     assert len(list(intpar.range)) == 1
     # Range contains ONLY the default / value.
     assert list(intpar.range) == [intpar.value]
-    intpar.hyperopt = True
+    intpar.in_space = True
 
     assert len(list(intpar.range)) == 6
     assert list(intpar.range) == [0, 1, 2, 3, 4, 5]
@@ -671,4 +671,4 @@ def test_auto_hyperopt_interface(default_conf):
     strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy')
 
     with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
-        [x for x in strategy.enumerate_parameters('sell')]
+        [x for x in strategy._detect_parameters('sell')]
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 785e866ae..4d9284a2f 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1371,7 +1371,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
     }
     mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order',
                  side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
+    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
+                 return_value=stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
 
@@ -2430,13 +2431,22 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
     freqtrade._notify_buy_cancel = MagicMock()
 
     trade = MagicMock()
-    trade.pair = 'LTC/ETH'
+    trade.pair = 'LTC/USDT'
+    trade.open_rate = 200
     limit_buy_order['filled'] = 0.0
     limit_buy_order['status'] = 'open'
     reason = CANCEL_REASON['TIMEOUT']
     assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
     assert cancel_order_mock.call_count == 1
 
+    cancel_order_mock.reset_mock()
+    caplog.clear()
+    limit_buy_order['filled'] = 0.01
+    assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
+    assert cancel_order_mock.call_count == 0
+    assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog)
+
+    caplog.clear()
     cancel_order_mock.reset_mock()
     limit_buy_order['filled'] = 2
     assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
@@ -2491,7 +2501,8 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
     freqtrade._notify_buy_cancel = MagicMock()
 
     trade = MagicMock()
-    trade.pair = 'LTC/ETH'
+    trade.pair = 'LTC/USDT'
+    trade.open_rate = 200
     limit_buy_order['filled'] = 0.0
     limit_buy_order['status'] = 'open'
     reason = CANCEL_REASON['TIMEOUT']
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 217910961..33b3e1140 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -63,7 +63,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
         fetch_stoploss_order=stoploss_order_mock,
-        cancel_stoploss_order=cancel_order_mock,
+        cancel_stoploss_order_with_result=cancel_order_mock,
     )
 
     mocker.patch.multiple(
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index 8470e12c2..669f220bb 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock
 
 import arrow
 import pytest
-from sqlalchemy import create_engine
+from sqlalchemy import create_engine, inspect
 
 from freqtrade import constants
 from freqtrade.exceptions import DependencyException, OperationalException
@@ -627,6 +627,63 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
     assert orders[1].order_id == 'stop_order_id222'
     assert orders[1].ft_order_side == 'stoploss'
 
+    caplog.clear()
+    # Drop latest column
+    engine.execute("alter table orders rename to orders_bak")
+    inspector = inspect(engine)
+
+    for index in inspector.get_indexes('orders_bak'):
+        engine.execute(f"drop index {index['name']}")
+    # Recreate table
+    engine.execute("""
+        CREATE TABLE orders (
+            id INTEGER NOT NULL,
+            ft_trade_id INTEGER,
+            ft_order_side VARCHAR NOT NULL,
+            ft_pair VARCHAR NOT NULL,
+            ft_is_open BOOLEAN NOT NULL,
+            order_id VARCHAR NOT NULL,
+            status VARCHAR,
+            symbol VARCHAR,
+            order_type VARCHAR,
+            side VARCHAR,
+            price FLOAT,
+            amount FLOAT,
+            filled FLOAT,
+            remaining FLOAT,
+            cost FLOAT,
+            order_date DATETIME,
+            order_filled_date DATETIME,
+            order_update_date DATETIME,
+            PRIMARY KEY (id),
+            CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id),
+            FOREIGN KEY(ft_trade_id) REFERENCES trades (id)
+        )
+        """)
+
+    engine.execute("""
+    insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
+        symbol, order_type, side, price, amount, filled, remaining, cost, order_date,
+        order_filled_date, order_update_date)
+        select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status,
+        symbol, order_type, side, price, amount, filled, remaining, cost, order_date,
+        order_filled_date, order_update_date
+        from orders_bak
+    """)
+
+    # Run init to test migration
+    init_db(default_conf['db_url'], default_conf['dry_run'])
+
+    assert log_has("trying orders_bak1", caplog)
+
+    orders = Order.query.all()
+    assert len(orders) == 2
+    assert orders[0].order_id == 'buy_order'
+    assert orders[0].ft_order_side == 'buy'
+
+    assert orders[1].order_id == 'stop_order_id222'
+    assert orders[1].ft_order_side == 'stoploss'
+
 
 def test_migrate_mid_state(mocker, default_conf, fee, caplog):
     """
diff --git a/tests/testdata/hyperopt_results_SampleStrategy.pickle b/tests/testdata/hyperopt_results_SampleStrategy.pickle
new file mode 100644
index 000000000..2231de7bf
Binary files /dev/null and b/tests/testdata/hyperopt_results_SampleStrategy.pickle differ
diff --git a/tests/testdata/strategy_SampleStrategy.fthypt b/tests/testdata/strategy_SampleStrategy.fthypt
new file mode 100644
index 000000000..6dc2b9ab1
--- /dev/null
+++ b/tests/testdata/strategy_SampleStrategy.fthypt
@@ -0,0 +1,5 @@
+{"loss":100000,"params_dict":{"mfi-value":"20","fastd-value":"21","adx-value":"26","rsi-value":"23","mfi-enabled":true,"fastd-enabled":false,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal","sell-mfi-value":"97","sell-fastd-value":"85","sell-adx-value":"55","sell-rsi-value":"76","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"34","roi_t2":"28","roi_t3":"32","roi_p1":0.031,"roi_p2":0.033,"roi_p3":0.146,"stoploss":-0.05},"params_details":{"buy":{"mfi-value":"20","fastd-value":"21","adx-value":"26","rsi-value":"23","mfi-enabled":true,"fastd-enabled":false,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"97","sell-fastd-value":"85","sell-adx-value":"55","sell-rsi-value":"76","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.21, 32: 0.064, 60: 0.031, 94: 0}","stoploss":{"stoploss":-0.05}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[],"locks":[],"best_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"worst_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"results_per_pair":[{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TRX/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"sell_reason_summary":[],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":0,"total_volume":0.0,"avg_stake_amount":0,"profit_mean":0,"profit_median":0,"profit_total":0.0,"profit_total_abs":0,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793107,"trades_per_day":0.0,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0,"backtest_worst_day":0,"backtest_best_day_abs":0,"backtest_worst_day_abs":0,"winning_days":0,"draw_days":0,"losing_days":0,"wins":0,"losses":0,"draws":0,"holding_avg":"0:00:00","winner_holding_avg":"0:00:00","loser_holding_avg":"0:00:00","max_drawdown":0.0,"max_drawdown_abs":0.0,"max_drawdown_low":0.0,"max_drawdown_high":0.0,"drawdown_start":"1970-01-01 00:00:00+00:00","drawdown_start_ts":0,"drawdown_end":"1970-01-01 00:00:00+00:00","drawdown_end_ts":0,"csum_min":0,"csum_max":0},"results_explanation":"     0 trades. 0/0/0 Wins/Draws/Losses. Avg profit   0.00%. Median profit   0.00%. Total profit  0.00000000 BTC (   0.00\u03A3%). Avg duration 0:00:00 min.","total_profit":0.0,"current_epoch":1,"is_initial_point":true,"is_best":false}
+{"loss":100000,"params_dict":{"mfi-value":"14","fastd-value":"43","adx-value":"30","rsi-value":"24","mfi-enabled":true,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal","sell-mfi-value":"97","sell-fastd-value":"71","sell-adx-value":"82","sell-rsi-value":"99","sell-mfi-enabled":false,"sell-fastd-enabled":false,"sell-adx-enabled":false,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"84","roi_t2":"35","roi_t3":"19","roi_p1":0.024,"roi_p2":0.022,"roi_p3":0.061,"stoploss":-0.083},"params_details":{"buy":{"mfi-value":"14","fastd-value":"43","adx-value":"30","rsi-value":"24","mfi-enabled":true,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"97","sell-fastd-value":"71","sell-adx-value":"82","sell-rsi-value":"99","sell-mfi-enabled":false,"sell-fastd-enabled":false,"sell-adx-enabled":false,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.107, 19: 0.046, 54: 0.024, 138: 0}","stoploss":{"stoploss":-0.083}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[],"locks":[],"best_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"worst_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"results_per_pair":[{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TRX/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"sell_reason_summary":[],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":0,"total_volume":0.0,"avg_stake_amount":0,"profit_mean":0,"profit_median":0,"profit_total":0.0,"profit_total_abs":0,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.0,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0,"backtest_worst_day":0,"backtest_best_day_abs":0,"backtest_worst_day_abs":0,"winning_days":0,"draw_days":0,"losing_days":0,"wins":0,"losses":0,"draws":0,"holding_avg":"0:00:00","winner_holding_avg":"0:00:00","loser_holding_avg":"0:00:00","max_drawdown":0.0,"max_drawdown_abs":0.0,"max_drawdown_low":0.0,"max_drawdown_high":0.0,"drawdown_start":"1970-01-01 00:00:00+00:00","drawdown_start_ts":0,"drawdown_end":"1970-01-01 00:00:00+00:00","drawdown_end_ts":0,"csum_min":0,"csum_max":0},"results_explanation":"     0 trades. 0/0/0 Wins/Draws/Losses. Avg profit   0.00%. Median profit   0.00%. Total profit  0.00000000 BTC (   0.00\u03A3%). Avg duration 0:00:00 min.","total_profit":0.0,"current_epoch":2,"is_initial_point":true,"is_best":false}
+{"loss":2.183447401951895,"params_dict":{"mfi-value":"14","fastd-value":"15","adx-value":"40","rsi-value":"36","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":false,"trigger":"sar_reversal","sell-mfi-value":"92","sell-fastd-value":"84","sell-adx-value":"61","sell-rsi-value":"61","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"68","roi_t2":"41","roi_t3":"21","roi_p1":0.015,"roi_p2":0.064,"roi_p3":0.126,"stoploss":-0.024},"params_details":{"buy":{"mfi-value":"14","fastd-value":"15","adx-value":"40","rsi-value":"36","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":false,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"92","sell-fastd-value":"84","sell-adx-value":"61","sell-rsi-value":"61","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.20500000000000002, 21: 0.079, 62: 0.015, 130: 0}","stoploss":{"stoploss":-0.024}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.94115571,"open_date":"2018-01-11 11:40:00+00:00","close_date":"2018-01-11 19:40:00+00:00","open_rate":0.01700012,"close_rate":0.017119538805820372,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":480,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.01659211712,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.01659211712,"stop_loss_ratio":-0.024,"min_rate":0.01689809,"max_rate":0.0171462,"is_open":false,"open_timestamp":1515670800000.0,"close_timestamp":1515699600000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.57407318,"open_date":"2018-01-12 11:05:00+00:00","close_date":"2018-01-12 12:30:00+00:00","open_rate":0.08709691,"close_rate":0.08901977203712995,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":0.08500658416,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.08500658416,"stop_loss_ratio":-0.024,"min_rate":0.08702974000000001,"max_rate":0.08929248000000001,"is_open":false,"open_timestamp":1515755100000.0,"close_timestamp":1515760200000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.93166236,"open_date":"2018-01-12 03:30:00+00:00","close_date":"2018-01-12 13:05:00+00:00","open_rate":0.01705517,"close_rate":0.01717497550928249,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":575,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016645845920000003,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016645845920000003,"stop_loss_ratio":-0.024,"min_rate":0.0169841,"max_rate":0.01719135,"is_open":false,"open_timestamp":1515727800000.0,"close_timestamp":1515762300000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.96876855,"open_date":"2018-01-13 03:50:00+00:00","close_date":"2018-01-13 06:05:00+00:00","open_rate":0.016842,"close_rate":0.016960308078273957,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016437792,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016437792,"stop_loss_ratio":-0.024,"min_rate":0.016836999999999998,"max_rate":0.017,"is_open":false,"open_timestamp":1515815400000.0,"close_timestamp":1515823500000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.53163205,"open_date":"2018-01-13 13:25:00+00:00","close_date":"2018-01-13 15:35:00+00:00","open_rate":0.09405001,"close_rate":0.09471067238835926,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":130,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.09179280976,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.09179280976,"stop_loss_ratio":-0.024,"min_rate":0.09369894000000001,"max_rate":0.09479997,"is_open":false,"open_timestamp":1515849900000.0,"close_timestamp":1515857700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.23816853,"open_date":"2018-01-13 15:30:00+00:00","close_date":"2018-01-13 16:20:00+00:00","open_rate":0.0025989999999999997,"close_rate":0.0028232990466633217,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":50,"profit_ratio":0.07872446,"profit_abs":0.00395,"sell_reason":"roi","initial_stop_loss_abs":0.002536624,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.002536624,"stop_loss_ratio":-0.024,"min_rate":0.00259525,"max_rate":0.0028288700000000003,"is_open":false,"open_timestamp":1515857400000.0,"close_timestamp":1515860400000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":492.80504632,"open_date":"2018-01-14 21:35:00+00:00","close_date":"2018-01-14 23:15:00+00:00","open_rate":0.00010146000000000001,"close_rate":0.00010369995985950828,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":100,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":9.902496e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.902496e-05,"stop_loss_ratio":-0.024,"min_rate":0.0001012,"max_rate":0.00010414,"is_open":false,"open_timestamp":1515965700000.0,"close_timestamp":1515971700000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.92398174,"open_date":"2018-01-15 12:45:00+00:00","close_date":"2018-01-15 21:05:00+00:00","open_rate":0.01709997,"close_rate":0.01722009021073758,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":500,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016689570719999998,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016689570719999998,"stop_loss_ratio":-0.024,"min_rate":0.01694,"max_rate":0.01725,"is_open":false,"open_timestamp":1516020300000.0,"close_timestamp":1516050300000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1111.60515785,"open_date":"2018-01-15 19:50:00+00:00","close_date":"2018-01-15 23:45:00+00:00","open_rate":4.4980000000000006e-05,"close_rate":4.390048e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":235,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":4.390048e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":4.390048e-05,"stop_loss_ratio":-0.024,"min_rate":4.409e-05,"max_rate":4.502e-05,"is_open":false,"open_timestamp":1516045800000.0,"close_timestamp":1516059900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":519.80455349,"open_date":"2018-01-21 03:55:00+00:00","close_date":"2018-01-21 04:05:00+00:00","open_rate":9.619e-05,"close_rate":9.388144e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":10,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":9.388144e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.388144e-05,"stop_loss_ratio":-0.024,"min_rate":9.568e-05,"max_rate":9.673e-05,"is_open":false,"open_timestamp":1516506900000.0,"close_timestamp":1516507500000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.029754,"open_date":"2018-01-20 22:15:00+00:00","close_date":"2018-01-21 07:45:00+00:00","open_rate":0.01650299,"close_rate":0.016106918239999997,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":570,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.016106918239999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016106918239999997,"stop_loss_ratio":-0.024,"min_rate":0.0162468,"max_rate":0.01663179,"is_open":false,"open_timestamp":1516486500000.0,"close_timestamp":1516520700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.75461832,"open_date":"2018-01-21 13:00:00+00:00","close_date":"2018-01-21 16:25:00+00:00","open_rate":0.00266601,"close_rate":0.00260202576,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":205,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.00260202576,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.00260202576,"stop_loss_ratio":-0.024,"min_rate":0.0026290800000000002,"max_rate":0.00269384,"is_open":false,"open_timestamp":1516539600000.0,"close_timestamp":1516551900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":552.18111541,"open_date":"2018-01-22 02:10:00+00:00","close_date":"2018-01-22 04:20:00+00:00","open_rate":9.055e-05,"close_rate":9.118607626693427e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":130,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":8.83768e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":8.83768e-05,"stop_loss_ratio":-0.024,"min_rate":9.013e-05,"max_rate":9.197e-05,"is_open":false,"open_timestamp":1516587000000.0,"close_timestamp":1516594800000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.99733237,"open_date":"2018-01-22 03:20:00+00:00","close_date":"2018-01-22 13:50:00+00:00","open_rate":0.0166815,"close_rate":0.016281143999999997,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":630,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.016281143999999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016281143999999997,"stop_loss_ratio":-0.024,"min_rate":0.01641443,"max_rate":0.016800000000000002,"is_open":false,"open_timestamp":1516591200000.0,"close_timestamp":1516629000000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":503.52467271,"open_date":"2018-01-23 08:55:00+00:00","close_date":"2018-01-23 09:40:00+00:00","open_rate":9.93e-05,"close_rate":9.69168e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":45,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":9.69168e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.69168e-05,"stop_loss_ratio":-0.024,"min_rate":9.754e-05,"max_rate":0.00010025,"is_open":false,"open_timestamp":1516697700000.0,"close_timestamp":1516700400000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.55148073,"open_date":"2018-01-24 02:10:00+00:00","close_date":"2018-01-24 04:40:00+00:00","open_rate":0.090665,"close_rate":0.09130188409433015,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":150,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.08848903999999999,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.08848903999999999,"stop_loss_ratio":-0.024,"min_rate":0.090665,"max_rate":0.09146000000000001,"is_open":false,"open_timestamp":1516759800000.0,"close_timestamp":1516768800000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.10584639,"open_date":"2018-01-24 19:20:00+00:00","close_date":"2018-01-24 21:35:00+00:00","open_rate":0.002617,"close_rate":0.0026353833416959357,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.002554192,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.002554192,"stop_loss_ratio":-0.024,"min_rate":0.002617,"max_rate":0.00264999,"is_open":false,"open_timestamp":1516821600000.0,"close_timestamp":1516829700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.34602691,"open_date":"2018-01-25 14:35:00+00:00","close_date":"2018-01-25 16:35:00+00:00","open_rate":0.00258451,"close_rate":0.002641568926241846,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":0.00252248176,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.00252248176,"stop_loss_ratio":-0.024,"min_rate":0.00258451,"max_rate":0.00264579,"is_open":false,"open_timestamp":1516890900000.0,"close_timestamp":1516898100000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.11910295,"open_date":"2018-01-24 23:05:00+00:00","close_date":"2018-01-25 16:55:00+00:00","open_rate":0.01603025,"close_rate":0.016142855870546913,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":1070,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.015645523999999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.015645523999999997,"stop_loss_ratio":-0.024,"min_rate":0.015798760000000002,"max_rate":0.01617,"is_open":false,"open_timestamp":1516835100000.0,"close_timestamp":1516899300000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":553.70985604,"open_date":"2018-01-26 19:30:00+00:00","close_date":"2018-01-26 23:30:00+00:00","open_rate":9.03e-05,"close_rate":9.093432012042147e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":240,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":8.813279999999999e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":8.813279999999999e-05,"stop_loss_ratio":-0.024,"min_rate":8.961e-05,"max_rate":9.1e-05,"is_open":false,"open_timestamp":1516995000000.0,"close_timestamp":1517009400000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.22929005,"open_date":"2018-01-26 21:15:00+00:00","close_date":"2018-01-28 03:50:00+00:00","open_rate":0.0026002,"close_rate":0.0026184653286502758,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":1835,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0025377952,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.0025377952,"stop_loss_ratio":-0.024,"min_rate":0.00254702,"max_rate":0.00262797,"is_open":false,"open_timestamp":1517001300000.0,"close_timestamp":1517111400000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.15677093,"open_date":"2018-01-27 22:05:00+00:00","close_date":"2018-01-28 10:45:00+00:00","open_rate":0.01583897,"close_rate":0.015950232207727046,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":760,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.01545883472,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.01545883472,"stop_loss_ratio":-0.024,"min_rate":0.015700000000000002,"max_rate":0.01596521,"is_open":false,"open_timestamp":1517090700000.0,"close_timestamp":1517136300000.0}],"locks":[],"best_pair":{"key":"ETC/BTC","trades":5,"profit_mean":0.012572794000000002,"profit_mean_pct":1.2572794000000003,"profit_sum":0.06286397,"profit_sum_pct":6.29,"profit_total_abs":0.0031542000000000002,"profit_total":3.1542000000000002e-06,"profit_total_pct":0.0,"duration_avg":"7:49:00","wins":2,"draws":2,"losses":1},"worst_pair":{"key":"LTC/BTC","trades":8,"profit_mean":-0.0077020425,"profit_mean_pct":-0.77020425,"profit_sum":-0.06161634,"profit_sum_pct":-6.16,"profit_total_abs":-0.0030916,"profit_total":-3.0915999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"9:50:00","wins":0,"draws":6,"losses":2},"results_per_pair":[{"key":"ETC/BTC","trades":5,"profit_mean":0.012572794000000002,"profit_mean_pct":1.2572794000000003,"profit_sum":0.06286397,"profit_sum_pct":6.29,"profit_total_abs":0.0031542000000000002,"profit_total":3.1542000000000002e-06,"profit_total_pct":0.0,"duration_avg":"7:49:00","wins":2,"draws":2,"losses":1},{"key":"ETH/BTC","trades":3,"profit_mean":0.00498256,"profit_mean_pct":0.498256,"profit_sum":0.01494768,"profit_sum_pct":1.49,"profit_total_abs":0.00075,"profit_total":7.5e-07,"profit_total_pct":0.0,"duration_avg":"2:02:00","wins":1,"draws":2,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":1,"profit_mean":-0.03080817,"profit_mean_pct":-3.080817,"profit_sum":-0.03080817,"profit_sum_pct":-3.08,"profit_total_abs":-0.0015458,"profit_total":-1.5457999999999999e-06,"profit_total_pct":-0.0,"duration_avg":"3:55:00","wins":0,"draws":0,"losses":1},{"key":"TRX/BTC","trades":5,"profit_mean":-0.009333732,"profit_mean_pct":-0.9333732000000001,"profit_sum":-0.04666866,"profit_sum_pct":-4.67,"profit_total_abs":-0.0023416,"profit_total":-2.3416e-06,"profit_total_pct":-0.0,"duration_avg":"1:45:00","wins":1,"draws":2,"losses":2},{"key":"LTC/BTC","trades":8,"profit_mean":-0.0077020425,"profit_mean_pct":-0.77020425,"profit_sum":-0.06161634,"profit_sum_pct":-6.16,"profit_total_abs":-0.0030916,"profit_total":-3.0915999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"9:50:00","wins":0,"draws":6,"losses":2},{"key":"TOTAL","trades":22,"profit_mean":-0.0027855236363636365,"profit_mean_pct":-0.27855236363636365,"profit_sum":-0.06128152,"profit_sum_pct":-6.13,"profit_total_abs":-0.0030748,"profit_total":-3.0747999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"6:12:00","wins":4,"draws":12,"losses":6}],"sell_reason_summary":[{"sell_reason":"roi","trades":16,"wins":4,"draws":12,"losses":0,"profit_mean":0.00772296875,"profit_mean_pct":0.77,"profit_sum":0.1235675,"profit_sum_pct":12.36,"profit_total_abs":0.006200000000000001,"profit_total":0.041189166666666666,"profit_total_pct":4.12},{"sell_reason":"stop_loss","trades":6,"wins":0,"draws":0,"losses":6,"profit_mean":-0.03080817,"profit_mean_pct":-3.08,"profit_sum":-0.18484902,"profit_sum_pct":-18.48,"profit_total_abs":-0.0092748,"profit_total":-0.06161634,"profit_total_pct":-6.16}],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":22,"total_volume":1.1000000000000003,"avg_stake_amount":0.05000000000000002,"profit_mean":-0.0027855236363636365,"profit_median":0.0,"profit_total":-3.0747999999999998e-06,"profit_total_abs":-0.0030748,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":1.16,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":999.9969252,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.07872446,"backtest_worst_day":-0.09242451,"backtest_best_day_abs":0.00395,"backtest_worst_day_abs":-0.0046374,"winning_days":4,"draw_days":10,"losing_days":4,"wins":4,"losses":6,"draws":12,"holding_avg":"6:12:00","winner_holding_avg":"1:29:00","loser_holding_avg":"4:42:00","max_drawdown":0.18484901999999998,"max_drawdown_abs":0.0092748,"drawdown_start":"2018-01-14 23:15:00","drawdown_start_ts":1515971700000.0,"drawdown_end":"2018-01-23 09:40:00","drawdown_end_ts":1516700400000.0,"max_drawdown_low":-0.0038247999999999997,"max_drawdown_high":0.00545,"csum_min":999.9961752,"csum_max":1000.00545},"results_explanation":"    22 trades. 4/12/6 Wins/Draws/Losses. Avg profit  -0.28%. Median profit   0.00%. Total profit -0.00307480 BTC (  -0.00\u03A3%). Avg duration 6:12:00 min.","total_profit":-3.0747999999999998e-06,"current_epoch":3,"is_initial_point":true,"is_best":true}
+{"loss":-4.9544427978437175,"params_dict":{"mfi-value":"23","fastd-value":"40","adx-value":"50","rsi-value":"27","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":true,"rsi-enabled":true,"trigger":"bb_lower","sell-mfi-value":"87","sell-fastd-value":"60","sell-adx-value":"81","sell-rsi-value":"69","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":false,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal","roi_t1":"105","roi_t2":"43","roi_t3":"12","roi_p1":0.03,"roi_p2":0.036,"roi_p3":0.103,"stoploss":-0.081},"params_details":{"buy":{"mfi-value":"23","fastd-value":"40","adx-value":"50","rsi-value":"27","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":true,"rsi-enabled":true,"trigger":"bb_lower"},"sell":{"sell-mfi-value":"87","sell-fastd-value":"60","sell-adx-value":"81","sell-rsi-value":"69","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":false,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal"},"roi":"{0: 0.16899999999999998, 12: 0.066, 55: 0.03, 160: 0}","stoploss":{"stoploss":-0.081}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"XLM/BTC","stake_amount":0.05,"amount":1086.95652174,"open_date":"2018-01-13 13:30:00+00:00","close_date":"2018-01-13 16:30:00+00:00","open_rate":4.6e-05,"close_rate":4.632313095835424e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":180,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.2274e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.2274e-05,"stop_loss_ratio":-0.081,"min_rate":4.4980000000000006e-05,"max_rate":4.673e-05,"is_open":false,"open_timestamp":1515850200000.0,"close_timestamp":1515861000000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":851.35365231,"open_date":"2018-01-15 14:50:00+00:00","close_date":"2018-01-15 16:15:00+00:00","open_rate":5.873000000000001e-05,"close_rate":6.0910642247867544e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":5.397287000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.397287000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.873000000000001e-05,"max_rate":6.120000000000001e-05,"is_open":false,"open_timestamp":1516027800000.0,"close_timestamp":1516032900000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":896.86098655,"open_date":"2018-01-16 00:35:00+00:00","close_date":"2018-01-16 03:15:00+00:00","open_rate":5.575000000000001e-05,"close_rate":5.6960000000000004e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.01457705,"profit_abs":0.0007314,"sell_reason":"roi","initial_stop_loss_abs":5.123425000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.123425000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.575000000000001e-05,"max_rate":5.730000000000001e-05,"is_open":false,"open_timestamp":1516062900000.0,"close_timestamp":1516072500000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":747.160789,"open_date":"2018-01-16 22:30:00+00:00","close_date":"2018-01-16 22:45:00+00:00","open_rate":6.692e-05,"close_rate":7.182231811339689e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":15,"profit_ratio":0.06576981,"profit_abs":0.0033,"sell_reason":"roi","initial_stop_loss_abs":6.149948000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":6.149948000000001e-05,"stop_loss_ratio":-0.081,"min_rate":6.692e-05,"max_rate":7.566e-05,"is_open":false,"open_timestamp":1516141800000.0,"close_timestamp":1516142700000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":720.5649229,"open_date":"2018-01-17 15:15:00+00:00","close_date":"2018-01-17 16:40:00+00:00","open_rate":6.939000000000001e-05,"close_rate":7.19664475664827e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":6.376941000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":6.376941000000001e-05,"stop_loss_ratio":-0.081,"min_rate":6.758e-05,"max_rate":7.244e-05,"is_open":false,"open_timestamp":1516202100000.0,"close_timestamp":1516207200000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1144.42664225,"open_date":"2018-01-18 22:20:00+00:00","close_date":"2018-01-19 00:35:00+00:00","open_rate":4.3690000000000004e-05,"close_rate":4.531220772704466e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":4.015111e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.015111e-05,"stop_loss_ratio":-0.081,"min_rate":4.3690000000000004e-05,"max_rate":4.779e-05,"is_open":false,"open_timestamp":1516314000000.0,"close_timestamp":1516322100000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":876.57784011,"open_date":"2018-01-18 22:25:00+00:00","close_date":"2018-01-19 01:05:00+00:00","open_rate":5.704e-05,"close_rate":5.792e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.00834457,"profit_abs":0.00041869,"sell_reason":"roi","initial_stop_loss_abs":5.2419760000000006e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.2419760000000006e-05,"stop_loss_ratio":-0.081,"min_rate":5.704e-05,"max_rate":5.8670000000000006e-05,"is_open":false,"open_timestamp":1516314300000.0,"close_timestamp":1516323900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":525.59655209,"open_date":"2018-01-20 05:05:00+00:00","close_date":"2018-01-20 06:25:00+00:00","open_rate":9.513e-05,"close_rate":9.86621726041144e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":80,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":8.742447000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":8.742447000000001e-05,"stop_loss_ratio":-0.081,"min_rate":9.513e-05,"max_rate":9.95e-05,"is_open":false,"open_timestamp":1516424700000.0,"close_timestamp":1516429500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":920.64076597,"open_date":"2018-01-26 07:40:00+00:00","close_date":"2018-01-26 10:20:00+00:00","open_rate":5.431000000000001e-05,"close_rate":5.474000000000001e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.0008867,"profit_abs":4.449e-05,"sell_reason":"roi","initial_stop_loss_abs":4.991089000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.991089000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.3670000000000006e-05,"max_rate":5.5e-05,"is_open":false,"open_timestamp":1516952400000.0,"close_timestamp":1516962000000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":944.28706327,"open_date":"2018-01-28 04:35:00+00:00","close_date":"2018-01-30 04:45:00+00:00","open_rate":5.2950000000000006e-05,"close_rate":4.995000000000001e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":2890,"profit_ratio":-0.06323759,"profit_abs":-0.00317295,"sell_reason":"force_sell","initial_stop_loss_abs":4.866105000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.866105000000001e-05,"stop_loss_ratio":-0.081,"min_rate":4.980000000000001e-05,"max_rate":5.3280000000000005e-05,"is_open":true,"open_timestamp":1517114100000.0,"close_timestamp":1517287500000.0}],"locks":[],"best_pair":{"key":"TRX/BTC","trades":3,"profit_mean":0.04185351666666667,"profit_mean_pct":4.185351666666667,"profit_sum":0.12556055,"profit_sum_pct":12.56,"profit_total_abs":0.0063,"profit_total":6.3e-06,"profit_total_pct":0.0,"duration_avg":"1:00:00","wins":3,"draws":0,"losses":0},"worst_pair":{"key":"XLM/BTC","trades":3,"profit_mean":-0.01111407333333333,"profit_mean_pct":-1.111407333333333,"profit_sum":-0.03334221999999999,"profit_sum_pct":-3.33,"profit_total_abs":-0.0016729499999999999,"profit_total":-1.6729499999999998e-06,"profit_total_pct":-0.0,"duration_avg":"17:48:00","wins":1,"draws":1,"losses":1},"results_per_pair":[{"key":"TRX/BTC","trades":3,"profit_mean":0.04185351666666667,"profit_mean_pct":4.185351666666667,"profit_sum":0.12556055,"profit_sum_pct":12.56,"profit_total_abs":0.0063,"profit_total":6.3e-06,"profit_total_pct":0.0,"duration_avg":"1:00:00","wins":3,"draws":0,"losses":0},{"key":"ADA/BTC","trades":4,"profit_mean":0.0134259225,"profit_mean_pct":1.34259225,"profit_sum":0.05370369,"profit_sum_pct":5.37,"profit_total_abs":0.00269458,"profit_total":2.69458e-06,"profit_total_pct":0.0,"duration_avg":"2:21:00","wins":4,"draws":0,"losses":0},{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":3,"profit_mean":-0.01111407333333333,"profit_mean_pct":-1.111407333333333,"profit_sum":-0.03334221999999999,"profit_sum_pct":-3.33,"profit_total_abs":-0.0016729499999999999,"profit_total":-1.6729499999999998e-06,"profit_total_pct":-0.0,"duration_avg":"17:48:00","wins":1,"draws":1,"losses":1},{"key":"TOTAL","trades":10,"profit_mean":0.014592201999999999,"profit_mean_pct":1.4592201999999999,"profit_sum":0.14592201999999999,"profit_sum_pct":14.59,"profit_total_abs":0.00732163,"profit_total":7.32163e-06,"profit_total_pct":0.0,"duration_avg":"6:35:00","wins":8,"draws":1,"losses":1}],"sell_reason_summary":[{"sell_reason":"roi","trades":9,"wins":8,"draws":1,"losses":0,"profit_mean":0.023239956666666665,"profit_mean_pct":2.32,"profit_sum":0.20915961,"profit_sum_pct":20.92,"profit_total_abs":0.01049458,"profit_total":0.06971987,"profit_total_pct":6.97},{"sell_reason":"force_sell","trades":1,"wins":0,"draws":0,"losses":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.32,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-0.021079196666666664,"profit_total_pct":-2.11}],"left_open_trades":[{"key":"XLM/BTC","trades":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.323759,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-3.17295e-06,"profit_total_pct":-0.0,"duration_avg":"2 days, 0:10:00","wins":0,"draws":0,"losses":1},{"key":"TOTAL","trades":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.323759,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-3.17295e-06,"profit_total_pct":-0.0,"duration_avg":"2 days, 0:10:00","wins":0,"draws":0,"losses":1}],"total_trades":10,"total_volume":0.5,"avg_stake_amount":0.05,"profit_mean":0.014592201999999999,"profit_median":0.02223621,"profit_total":7.32163e-06,"profit_total_abs":0.00732163,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.53,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000.00732163,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.08034685999999999,"backtest_worst_day":-0.06323759,"backtest_best_day_abs":0.0040314,"backtest_worst_day_abs":-0.00317295,"winning_days":6,"draw_days":11,"losing_days":1,"wins":8,"losses":1,"draws":1,"holding_avg":"6:35:00","winner_holding_avg":"1:50:00","loser_holding_avg":"2 days, 0:10:00","max_drawdown":0.06323759000000001,"max_drawdown_abs":0.00317295,"drawdown_start":"2018-01-26 10:20:00","drawdown_start_ts":1516962000000.0,"drawdown_end":"2018-01-30 04:45:00","drawdown_end_ts":1517287500000.0,"max_drawdown_low":0.007321629999999998,"max_drawdown_high":0.010494579999999998,"csum_min":1000.0,"csum_max":1000.01049458},"results_explanation":"    10 trades. 8/1/1 Wins/Draws/Losses. Avg profit   1.46%. Median profit   2.22%. Total profit  0.00732163 BTC (   0.00\u03A3%). Avg duration 6:35:00 min.","total_profit":7.32163e-06,"current_epoch":4,"is_initial_point":true,"is_best":true}
+{"loss":0.16709185414267655,"params_dict":{"mfi-value":"10","fastd-value":"45","adx-value":"28","rsi-value":"37","mfi-enabled":false,"fastd-enabled":false,"adx-enabled":true,"rsi-enabled":true,"trigger":"macd_cross_signal","sell-mfi-value":"85","sell-fastd-value":"56","sell-adx-value":"98","sell-rsi-value":"89","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal","roi_t1":"85","roi_t2":"11","roi_t3":"24","roi_p1":0.04,"roi_p2":0.043,"roi_p3":0.053,"stoploss":-0.057},"params_details":{"buy":{"mfi-value":"10","fastd-value":"45","adx-value":"28","rsi-value":"37","mfi-enabled":false,"fastd-enabled":false,"adx-enabled":true,"rsi-enabled":true,"trigger":"macd_cross_signal"},"sell":{"sell-mfi-value":"85","sell-fastd-value":"56","sell-adx-value":"98","sell-rsi-value":"89","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal"},"roi":"{0: 0.13599999999999998, 24: 0.08299999999999999, 35: 0.04, 120: 0}","stoploss":{"stoploss":-0.057}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.56173464,"open_date":"2018-01-10 19:15:00+00:00","close_date":"2018-01-10 21:15:00+00:00","open_rate":0.08901,"close_rate":0.09112999000000001,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.01667571,"profit_abs":0.0008367,"sell_reason":"roi","initial_stop_loss_abs":0.08393643,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.08393643,"stop_loss_ratio":-0.057,"min_rate":0.08894498,"max_rate":0.09116998,"is_open":false,"open_timestamp":1515611700000.0,"close_timestamp":1515618900000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":794.65988557,"open_date":"2018-01-13 11:30:00+00:00","close_date":"2018-01-13 15:10:00+00:00","open_rate":6.292e-05,"close_rate":5.9333559999999994e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":220,"profit_ratio":-0.06357798,"profit_abs":-0.00319003,"sell_reason":"stop_loss","initial_stop_loss_abs":5.9333559999999994e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.9333559999999994e-05,"stop_loss_ratio":-0.057,"min_rate":5.9900000000000006e-05,"max_rate":6.353e-05,"is_open":false,"open_timestamp":1515843000000.0,"close_timestamp":1515856200000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1086.95652174,"open_date":"2018-01-13 14:35:00+00:00","close_date":"2018-01-13 21:40:00+00:00","open_rate":4.6e-05,"close_rate":4.632313095835424e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":425,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.3378e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":4.3378e-05,"stop_loss_ratio":-0.057,"min_rate":4.4980000000000006e-05,"max_rate":4.6540000000000005e-05,"is_open":false,"open_timestamp":1515854100000.0,"close_timestamp":1515879600000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.53757603,"open_date":"2018-01-15 13:15:00+00:00","close_date":"2018-01-15 15:15:00+00:00","open_rate":0.0930101,"close_rate":0.09366345745107878,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0877085243,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0877085243,"stop_loss_ratio":-0.057,"min_rate":0.09188489999999999,"max_rate":0.09380000000000001,"is_open":false,"open_timestamp":1516022100000.0,"close_timestamp":1516029300000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":17.07469496,"open_date":"2018-01-15 14:35:00+00:00","close_date":"2018-01-15 16:35:00+00:00","open_rate":0.00292831,"close_rate":0.00297503,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.00886772,"profit_abs":0.00044494,"sell_reason":"roi","initial_stop_loss_abs":0.0027613963299999997,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0027613963299999997,"stop_loss_ratio":-0.057,"min_rate":0.00292831,"max_rate":0.00301259,"is_open":false,"open_timestamp":1516026900000.0,"close_timestamp":1516034100000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":702.44450688,"open_date":"2018-01-17 04:25:00+00:00","close_date":"2018-01-17 05:00:00+00:00","open_rate":7.118e-05,"close_rate":7.453721023582538e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":35,"profit_ratio":0.03986049,"profit_abs":0.002,"sell_reason":"roi","initial_stop_loss_abs":6.712274e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":6.712274e-05,"stop_loss_ratio":-0.057,"min_rate":7.118e-05,"max_rate":7.658000000000002e-05,"is_open":false,"open_timestamp":1516163100000.0,"close_timestamp":1516165200000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.86756854,"open_date":"2018-01-20 06:05:00+00:00","close_date":"2018-01-20 08:05:00+00:00","open_rate":0.00265005,"close_rate":0.00266995,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.00048133,"profit_abs":2.415e-05,"sell_reason":"roi","initial_stop_loss_abs":0.00249899715,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.00249899715,"stop_loss_ratio":-0.057,"min_rate":0.00265005,"max_rate":0.00271,"is_open":false,"open_timestamp":1516428300000.0,"close_timestamp":1516435500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":966.18357488,"open_date":"2018-01-22 03:25:00+00:00","close_date":"2018-01-22 07:05:00+00:00","open_rate":5.1750000000000004e-05,"close_rate":5.211352232814853e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":220,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.8800250000000004e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":4.8800250000000004e-05,"stop_loss_ratio":-0.057,"min_rate":5.1750000000000004e-05,"max_rate":5.2170000000000004e-05,"is_open":false,"open_timestamp":1516591500000.0,"close_timestamp":1516604700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.95303438,"open_date":"2018-01-23 13:10:00+00:00","close_date":"2018-01-23 16:00:00+00:00","open_rate":0.0026381,"close_rate":0.002656631560461616,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":170,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0024877283,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0024877283,"stop_loss_ratio":-0.057,"min_rate":0.0026100000000000003,"max_rate":0.00266,"is_open":false,"open_timestamp":1516713000000.0,"close_timestamp":1516723200000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":912.40875912,"open_date":"2018-01-26 06:30:00+00:00","close_date":"2018-01-26 10:45:00+00:00","open_rate":5.480000000000001e-05,"close_rate":5.518494731560462e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":255,"profit_ratio":-0.0,"profit_abs":-0.0,"sell_reason":"roi","initial_stop_loss_abs":5.1676400000000006e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.1676400000000006e-05,"stop_loss_ratio":-0.057,"min_rate":5.3670000000000006e-05,"max_rate":5.523e-05,"is_open":false,"open_timestamp":1516948200000.0,"close_timestamp":1516963500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":909.58704748,"open_date":"2018-01-27 02:10:00+00:00","close_date":"2018-01-27 05:40:00+00:00","open_rate":5.4970000000000004e-05,"close_rate":5.535614149523332e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":210,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":5.183671e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.183671e-05,"stop_loss_ratio":-0.057,"min_rate":5.472000000000001e-05,"max_rate":5.556e-05,"is_open":false,"open_timestamp":1517019000000.0,"close_timestamp":1517031600000.0}],"locks":[],"best_pair":{"key":"TRX/BTC","trades":1,"profit_mean":0.03986049,"profit_mean_pct":3.986049,"profit_sum":0.03986049,"profit_sum_pct":3.99,"profit_total_abs":0.002,"profit_total":2e-06,"profit_total_pct":0.0,"duration_avg":"0:35:00","wins":1,"draws":0,"losses":0},"worst_pair":{"key":"ADA/BTC","trades":4,"profit_mean":-0.015894495,"profit_mean_pct":-1.5894495000000002,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-3.19003e-06,"profit_total_pct":-0.0,"duration_avg":"3:46:00","wins":0,"draws":3,"losses":1},"results_per_pair":[{"key":"TRX/BTC","trades":1,"profit_mean":0.03986049,"profit_mean_pct":3.986049,"profit_sum":0.03986049,"profit_sum_pct":3.99,"profit_total_abs":0.002,"profit_total":2e-06,"profit_total_pct":0.0,"duration_avg":"0:35:00","wins":1,"draws":0,"losses":0},{"key":"ETH/BTC","trades":2,"profit_mean":0.008337855,"profit_mean_pct":0.8337855,"profit_sum":0.01667571,"profit_sum_pct":1.67,"profit_total_abs":0.0008367,"profit_total":8.367e-07,"profit_total_pct":0.0,"duration_avg":"2:00:00","wins":1,"draws":1,"losses":0},{"key":"ETC/BTC","trades":3,"profit_mean":0.0031163500000000004,"profit_mean_pct":0.31163500000000005,"profit_sum":0.009349050000000001,"profit_sum_pct":0.93,"profit_total_abs":0.00046909,"profit_total":4.6909000000000003e-07,"profit_total_pct":0.0,"duration_avg":"2:17:00","wins":2,"draws":1,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":1,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"7:05:00","wins":0,"draws":1,"losses":0},{"key":"ADA/BTC","trades":4,"profit_mean":-0.015894495,"profit_mean_pct":-1.5894495000000002,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-3.19003e-06,"profit_total_pct":-0.0,"duration_avg":"3:46:00","wins":0,"draws":3,"losses":1},{"key":"TOTAL","trades":11,"profit_mean":0.00020975181818181756,"profit_mean_pct":0.020975181818181757,"profit_sum":0.002307269999999993,"profit_sum_pct":0.23,"profit_total_abs":0.00011576000000000034,"profit_total":1.1576000000000034e-07,"profit_total_pct":0.0,"duration_avg":"3:03:00","wins":4,"draws":6,"losses":1}],"sell_reason_summary":[{"sell_reason":"roi","trades":10,"wins":4,"draws":6,"losses":0,"profit_mean":0.0065885250000000005,"profit_mean_pct":0.66,"profit_sum":0.06588525,"profit_sum_pct":6.59,"profit_total_abs":0.0033057900000000003,"profit_total":0.021961750000000002,"profit_total_pct":2.2},{"sell_reason":"stop_loss","trades":1,"wins":0,"draws":0,"losses":1,"profit_mean":-0.06357798,"profit_mean_pct":-6.36,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-0.021192660000000002,"profit_total_pct":-2.12}],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":11,"total_volume":0.55,"avg_stake_amount":0.05,"profit_mean":0.00020975181818181756,"profit_median":0.0,"profit_total":1.1576000000000034e-07,"profit_total_abs":0.00011576000000000034,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.58,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000.00011576,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.03986049,"backtest_worst_day":-0.06357798,"backtest_best_day_abs":0.002,"backtest_worst_day_abs":-0.00319003,"winning_days":4,"draw_days":13,"losing_days":1,"wins":4,"losses":1,"draws":6,"holding_avg":"3:03:00","winner_holding_avg":"1:39:00","loser_holding_avg":"3:40:00","max_drawdown":0.06357798,"max_drawdown_abs":0.00319003,"drawdown_start":"2018-01-10 21:15:00","drawdown_start_ts":1515618900000.0,"drawdown_end":"2018-01-13 15:10:00","drawdown_end_ts":1515856200000.0,"max_drawdown_low":-0.00235333,"max_drawdown_high":0.0008367,"csum_min":999.99764667,"csum_max":1000.0008367},"results_explanation":"    11 trades. 4/6/1 Wins/Draws/Losses. Avg profit   0.02%. Median profit   0.00%. Total profit  0.00011576 BTC (   0.00\u03A3%). Avg duration 3:03:00 min.","total_profit":1.1576000000000034e-07,"current_epoch":5,"is_initial_point":true,"is_best":false}