From 1b4b10f8cdfb2e04f0acba0d6f9f674e8773ecbf Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Tue, 23 Jul 2019 23:45:27 -0500 Subject: [PATCH 001/281] Update docs/installation.md Address that numpy is required before `python3 -m pip install -r requirements.txt` can run. --- docs/installation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation.md b/docs/installation.md index 657273e2f..35cdcda62 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -175,6 +175,7 @@ cp config.json.example config.json ``` bash python3 -m pip install --upgrade pip +pip install numpy python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` From 28411da83eedd4d0441e1c8f5836d0f3a0864e39 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Tue, 22 Sep 2020 22:28:12 +0100 Subject: [PATCH 002/281] Add the telegram command function template. --- freqtrade/rpc/telegram.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a01efaed6..a2dae387e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -108,6 +108,7 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('stats', self._stats), ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -738,6 +739,19 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) + @authorized_only + def _stats(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /stats + https://github.com/freqtrade/freqtrade/issues/3783 + Show stats of recent trades + :param update: message update + :return: None + """ + # TODO: self._send_msg(...) + trades = self._rpc_trade_history(-1) + + @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 44ad0f631c00ed00063fa61ebe3aefd6b5f736a1 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sat, 26 Sep 2020 22:40:54 +0100 Subject: [PATCH 003/281] Summarize trade reason for telegram command /stats. --- freqtrade/rpc/telegram.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a2dae387e..47e9d67dc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -749,9 +749,40 @@ class Telegram(RPC): :return: None """ # TODO: self._send_msg(...) - trades = self._rpc_trade_history(-1) - + def trade_win_loss(trade): + if trade['profit_abs'] > 0: + return 'Wins' + elif trade['profit_abs'] < 0: + return 'Losses' + else: + return 'Draws' + trades = self._rpc_trade_history(-1) + trades_closed = [trade for trade in trades if not trade['is_open']] + + # Sell reason + sell_reasons = {} + for trade in trades_closed: + if trade['sell_reason'] in sell_reasons: + sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 + else: + win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} + win_loss_count[trade_win_loss(trade)] += 1 + sell_reasons[trade['sell_reason']] = win_loss_count + sell_reason_msg = [ + '| Sell Reason | Sells | Wins | Draws | Losses |', + '|-------------|------:|-----:|------:|-------:|' + ] + # | Sell Reason | Sells | Wins | Draws | Losses | + # |-------------|------:|-----:|------:|-------:| + # | test | 1 | 2 | 3 | 4 | + for reason, count in sell_reasons.items(): + msg = f'| `{reason}` | `{sum(count.values())}` | `{count['Wins']}` | `{count['Draws']}` | `{count['Losses']}` |' + sell_reason_msg.append(msg) + + # TODO: Duration + + @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 627e221b654e5b0ebf87d6f299f70e26c798e3b7 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sun, 27 Sep 2020 20:23:13 +0100 Subject: [PATCH 004/281] Use tabulate to create sell reason message. --- freqtrade/rpc/telegram.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 47e9d67dc..ea8597469 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -769,16 +769,21 @@ class Telegram(RPC): win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} win_loss_count[trade_win_loss(trade)] += 1 sell_reasons[trade['sell_reason']] = win_loss_count - sell_reason_msg = [ - '| Sell Reason | Sells | Wins | Draws | Losses |', - '|-------------|------:|-----:|------:|-------:|' - ] + sell_reasons_tabulate = [] # | Sell Reason | Sells | Wins | Draws | Losses | # |-------------|------:|-----:|------:|-------:| # | test | 1 | 2 | 3 | 4 | for reason, count in sell_reasons.items(): - msg = f'| `{reason}` | `{sum(count.values())}` | `{count['Wins']}` | `{count['Draws']}` | `{count['Losses']}` |' - sell_reason_msg.append(msg) + sell_reasons_tabulate.append([ + reason, sum(count.values()), + count['Wins'], + count['Draws'], + count['Losses'] + ]) + sell_reasons_msg = tabulate( + sell_reasons_tabulate, + headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] + ) # TODO: Duration From 7bce2cd29daa65a8013d0f2f44fee817901b2465 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Mon, 28 Sep 2020 20:30:20 +0100 Subject: [PATCH 005/281] Add trade duration by win/loss. --- freqtrade/rpc/telegram.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ea8597469..bfe486951 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -763,16 +763,10 @@ class Telegram(RPC): # Sell reason sell_reasons = {} for trade in trades_closed: - if trade['sell_reason'] in sell_reasons: - sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 - else: - win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} - win_loss_count[trade_win_loss(trade)] += 1 - sell_reasons[trade['sell_reason']] = win_loss_count + if trade['sell_reason'] not in sell_reasons: + sell_reasons[trade['sell_reason']] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] - # | Sell Reason | Sells | Wins | Draws | Losses | - # |-------------|------:|-----:|------:|-------:| - # | test | 1 | 2 | 3 | 4 | for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ reason, sum(count.values()), @@ -785,9 +779,22 @@ class Telegram(RPC): headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] ) - # TODO: Duration + # Duration + dur = {'Wins': [], 'Draws': [], 'Losses': []} + for trade in trades_closed: + if trade['close_date'] is not None and trade['open_date'] is not None: + trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) + dur[trade_win_loss(trade)].append(trade_dur) + wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' + draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' + losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + duration_msg = tabulate( + [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], + headers=['', 'Duration'] + ) + + self._send_msg('\n'.join([sell_reasons_msg, duration_msg])) - @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 355afc082e4619e7de11640bae4b6dfc8cc61f81 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Mon, 5 Oct 2020 10:05:15 +0100 Subject: [PATCH 006/281] Add command 'stats' in expected test output. --- tests/rpc/test_rpc_telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 762780111..bcb9abc85 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -78,7 +78,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version'], " + "['stats']]") assert log_has(message_str, caplog) From 1c27aaab724b781487a92f103363071d1276c18f Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sun, 18 Oct 2020 20:24:13 +0100 Subject: [PATCH 007/281] Declare type of 'dur'. --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bfe486951..8404625f1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,7 +6,7 @@ This module manage Telegram communication import json import logging import arrow -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update @@ -780,7 +780,7 @@ class Telegram(RPC): ) # Duration - dur = {'Wins': [], 'Draws': [], 'Losses': []} + dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} for trade in trades_closed: if trade['close_date'] is not None and trade['open_date'] is not None: trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) From 8e03fee8685ccbb8098cabb81bc622498e8dbf53 Mon Sep 17 00:00:00 2001 From: radwayne <73605415+radwayne@users.noreply.github.com> Date: Fri, 6 Nov 2020 13:56:46 +0100 Subject: [PATCH 008/281] Update interface.py Changed The should_sell() method, to handle the case where both ROI and trailing stoploss are reached in backtest. --- freqtrade/strategy/interface.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1c6aa535d..44a281ebe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -475,16 +475,27 @@ class IStrategy(ABC): stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) - - if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " - f"sell_type={stoplossflag.sell_type}") - return stoplossflag - + # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) + + roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, + current_time=date) + + if stoplossflag.sell_flag: + + # When backtesting, in the case of trailing_stop_loss, + # make sure we don't make a profit higher than ROI. + if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: + logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " + f"sell_type=SellType.ROI") + return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + f"sell_type={stoplossflag.sell_type}") + return stoplossflag if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): # This one is noisy, commented out @@ -492,7 +503,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): + if roi_reached: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) From 181d3a3808412f35be91165e9e092bc0ea5209ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2020 05:49:32 +0000 Subject: [PATCH 009/281] Bump python from 3.8.6-slim-buster to 3.9.0-slim-buster Bumps python from 3.8.6-slim-buster to 3.9.0-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2be65274e..68b37afe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.6-slim-buster +FROM python:3.9.0-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From dd42d61d03d584051b39c4ada5132f9d223bcdbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Oct 2020 08:48:45 +0200 Subject: [PATCH 010/281] Run CI on 3.9 --- .github/workflows/ci.yml | 4 ++-- setup.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f259129d4..d48dec2d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -119,7 +119,7 @@ jobs: strategy: matrix: os: [ windows-latest ] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/setup.sh b/setup.sh index 049a6a77e..419fde9bb 100755 --- a/setup.sh +++ b/setup.sh @@ -42,7 +42,7 @@ function check_installed_python() { fi if [ -z ${PYTHON} ]; then - echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" + echo "No usable python found. Please make sure to have python3.6, python3.7 or python3.8 installed" exit 1 fi } From 312533fded2c794798a860b3ad523aec6f10ecc1 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Mon, 23 Nov 2020 22:08:53 -0600 Subject: [PATCH 011/281] Match current dev file --- docs/installation.md | 249 +++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 138 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 35cdcda62..ec2d27174 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,6 +2,8 @@ This page explains how to prepare your environment for running the bot. +Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. + ## Prerequisite ### Requirements @@ -11,70 +13,78 @@ Click each one for install guide: * [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) * [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) -### API keys + We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. -Before running your bot in production you will need to setup few -external API. In production mode, the bot will require valid Exchange API -credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - -### Setup your exchange account - -You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ## Quick start -Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. - -!!! Note - Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case. - -```bash -git clone git@github.com:freqtrade/freqtrade.git -cd freqtrade -git checkout develop -./setup.sh --install -``` +Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot. !!! Note Windows installation is explained [here](#windows). -## Easy Installation - Linux Script +The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform. -If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. +!!! Note "Version considerations" + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). + +!!! Note + Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + +This can be achieved with the following commands: + +```bash +git clone https://github.com/freqtrade/freqtrade.git +cd freqtrade +# git checkout stable # Optional, see (1) +./setup.sh --install +``` + +(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. + +## Easy Installation Script (Linux/MacOS) + +If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. ```bash $ ./setup.sh usage: -i,--install Install freqtrade from scratch -u,--update Command git pull to update. - -r,--reset Hard reset your develop/master branch. + -r,--reset Hard reset your develop/stable branch. -c,--config Easy config generator (Will override your existing file). ``` ** --install ** -This script will install everything you need to run the bot: +With this option, the script will install the bot and most dependencies: +You will need to have git and python3.6+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv -* Configure your `config.json` file +* Setup your virtualenv under `.env/` -This script is a combination of `install script` `--reset`, `--config` +This option is a combination of installation tasks, `--reset` and `--config`. ** --update ** -Update parameter will pull the last version of your current branch and update your virtualenv. +This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. ** --reset ** -Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv. +This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. ** --config ** -Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. +DEPRECATED - use `freqtrade new-config -c config.json` instead. + +### Activate your virtual environment + +Each time you open a new terminal, you must run `source .env/bin/activate`. ------ @@ -86,40 +96,50 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -### Linux - Ubuntu 16.04 +=== "Ubuntu 16.04" + #### Install necessary dependencies -#### Install necessary dependencies + ```bash + sudo apt-get update + sudo apt-get install build-essential git + ``` -```bash -sudo apt-get update -sudo apt-get install build-essential git -``` +=== "RaspberryPi/Raspbian" + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. -#### Raspberry Pi / Raspbian + Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. -Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/). + ``` bash + sudo apt-get install python3-venv libatlas-base-dev + git clone https://github.com/freqtrade/freqtrade.git + cd freqtrade -The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. -It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. + bash setup.sh -i + ``` -Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). + !!! Note "Installation duration" + Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. -``` bash -conda config --add channels rpi -conda install python=3.6 -conda create -n freqtrade python=3.6 -conda activate freqtrade -conda install scipy pandas numpy - -sudo apt install libffi-dev -python3 -m pip install -r requirements-common.txt -python3 -m pip install -e . -``` + !!! Note + The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. + We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. ### Common #### 1. Install TA-Lib +Use the provided ta-lib installation script + +```bash +sudo ./build_helpers/install_ta-lib.sh +``` + +!!! Note + This will use the ta-lib tar.gz included in this repository. + +##### TA-Lib manual installation + Official webpage: https://mrjbq7.github.io/ta-lib/install.html ```bash @@ -147,126 +167,79 @@ python3 -m venv .env source .env/bin/activate ``` -#### 3. Install FreqTrade +#### 3. Install Freqtrade Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git - -``` - -Optionally checkout the master branch to get the latest stable release: - -```bash -git checkout master -``` - -#### 4. Initialize the configuration - -```bash cd freqtrade -cp config.json.example config.json +git checkout stable ``` -> *To edit the config please refer to [Bot Configuration](configuration.md).* - -#### 5. Install python dependencies +#### 4. Install python dependencies ``` bash python3 -m pip install --upgrade pip -pip install numpy -python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` +#### 5. Initialize the configuration + +```bash +# Initialize the user_directory +freqtrade create-userdir --userdir user_data/ + +# Create a new configuration file +freqtrade new-config --config config.json +``` + +> *To edit the config please refer to [Bot Configuration](configuration.md).* + #### 6. Run the Bot If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -freqtrade -c config.json +freqtrade trade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. -#### 7. [Optional] Configure `freqtrade` as a `systemd` service +#### 7. (Optional) Post-installation Tasks -From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. - -After that you can start the daemon with: - -```bash -systemctl --user start freqtrade -``` - -For this to be persistent (run when user is logged out) you'll need to enable `linger` for your freqtrade user. - -```bash -sudo loginctl enable-linger "$USER" -``` - -If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot -state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the -configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd -using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped) -when it changes. - -The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd -as the watchdog. - -!!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. +On Linux, as an optional post-installation task, you may wish to setup the bot to run as a `systemd` service or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details. ------ -## Windows +### Anaconda -We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). +Freqtrade can also be installed using Anaconda (or Miniconda). -If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. -If that is not available on your system, feel free to try the instructions below, which led to success for some. - -### Install freqtrade manually - -#### Clone the git repository - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -#### Install ta-lib - -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.17‑cp36‑cp36m‑win32.whl` (make sure to use the version matching your python version) - -```cmd ->cd \path\freqtrade-develop ->python -m venv .env ->cd .env\Scripts ->activate.bat ->cd \path\freqtrade-develop -REM optionally install ta-lib from wheel -REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl ->pip install -r requirements.txt ->pip install -e . ->python freqtrade\main.py -``` - -> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) - -#### Error during installation under Windows +!!! Note + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. ``` bash -error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools +conda env create -f environment.yml ``` -Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. +----- +## Troubleshooting -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. +### MacOS installation error ---- +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. + +This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +For MacOS 10.14, this can be accomplished with the below command. + +``` bash +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. + +----- Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). +[Bot Configuration](configuration.md). \ No newline at end of file From 81d08c4deff25a9bd8b24e6d6847bdc04ccdace0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:16:11 +0100 Subject: [PATCH 012/281] Add detailed backtest test verifying the ROI / trailing stop collision --- freqtrade/strategy/interface.py | 12 +++--- tests/optimize/test_backtest_detail.py | 55 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 44a281ebe..172264b10 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -475,24 +475,24 @@ class IStrategy(ABC): 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 current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) - + roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date) - + current_time=date) + if stoplossflag.sell_flag: - + # When backtesting, in the case of trailing_stop_loss, # make sure we don't make a profit higher than ROI. if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " f"sell_type={stoplossflag.sell_type}") return stoplossflag diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a5de64fe4..f3a2d8b96 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -328,6 +328,58 @@ tc20 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 21: trailing_stop ROI collision. +# Roi should trigger before Trailing stop - otherwise Trailing stop profits can be > ROI +# which cannot happen in reality +# stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the sell candle +tc21 = 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, 4650, 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.04}, profit_perc=0.04, 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.ROI, open_tick=1, close_tick=2)] +) + +# Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time. +# applying a positive trailing stop of 3% - ROI should apply before trailing stop. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2 +tc22 = 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.04}, profit_perc=0.04, 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.ROI, open_tick=1, close_tick=2)] +) + +# Test 23: trailing_stop Raises in candle 2 (does not trigger) +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing a sell +# in the candle after the raised stoploss candle with ROI reason. +# Stoploss would trigger in this candle too, but it's no longer relevant. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the sell) +tc23 = 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, 5251, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, 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.ROI, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, @@ -351,6 +403,9 @@ TESTS = [ tc18, tc19, tc20, + tc21, + tc22, + tc23, ] From 57461a59f3b416add4e2a5169d89b81cd4c12ea8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:30:17 +0100 Subject: [PATCH 013/281] Update backtesting documentation with new logic --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index 84911568b..2121e3126 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -278,6 +278,7 @@ Since backtesting lacks some detailed information about what happens within a ca - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) + - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) - Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. From 4aa6ebee049a924b5fd5c931f14360af917eb3e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:17:25 +0100 Subject: [PATCH 014/281] Add more tests for #2422 --- tests/optimize/test_backtest_detail.py | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f3a2d8b96..720ed8c13 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -380,6 +380,66 @@ tc23 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Stoploss wins over Sell-signal (because sell-signal is acted on in the next candle) +tc24 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5000, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal + [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] +) + +# Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5000, 4986, 5010, 6172, 0, 1], + [4, 5010, 4987, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + +# Test 26: Sell with signal sell in candle 3 (ROI at signal candle) +# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) +# Sell-signal wins over stoploss +tc26 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal + [4, 5010, 4987, 4855, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 27: Sell with signal sell in candle 3 (ROI at signal candle) +# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over Sell-signal +# TODO: figure out if sell-signal should win over ROI +# Sell-signal wins over stoploss +tc27 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal + [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] +) TESTS = [ tc0, @@ -406,6 +466,10 @@ TESTS = [ tc21, tc22, tc23, + tc24, + tc25, + tc26, + tc27, ] From fefb4b23d0603683800790de198f1894143449a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:18:03 +0100 Subject: [PATCH 015/281] revise logic in should_sell --- freqtrade/strategy/interface.py | 53 ++++++++++++++------------------- tests/test_freqtradebot.py | 2 +- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 172264b10..81f4e7651 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -481,46 +481,39 @@ class IStrategy(ABC): current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) - roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date) + # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. + roi_reached = (not (buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False)) + and self.min_roi_reached(trade=trade, current_profit=current_profit, + current_time=date)) - if stoplossflag.sell_flag: + if config_ask_strategy.get('sell_profit_only', False) and trade.calc_profit(rate=rate) <= 0: + # Negative profits and sell_profit_only - ignore sell signal + sell_signal = False + else: + sell_signal = sell and not buy and config_ask_strategy.get('use_sell_signal', True) + # TODO: return here if sell-signal should be favored over ROI - # When backtesting, in the case of trailing_stop_loss, - # make sure we don't make a profit higher than ROI. - if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: - logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " - f"sell_type=SellType.ROI") - return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " - f"sell_type={stoplossflag.sell_type}") - return stoplossflag - - if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - - # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if roi_reached: + # Start evaluations + # Sequence: + # ROI (if not stoploss) + # Sell-signal + # Stoploss + if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if config_ask_strategy.get('sell_profit_only', False): - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Checking if trade is profitable...") - if trade.calc_profit(rate=rate) <= 0: - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - - if sell and not buy and config_ask_strategy.get('use_sell_signal', True): + if sell_signal: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " f"sell_type=SellType.SELL_SIGNAL") return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + if stoplossflag.sell_flag: + + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + f"sell_type={stoplossflag.sell_type}") + return stoplossflag + # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False") return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1f5b3ecaa..64dfb016e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3556,7 +3556,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b # Test if buy-signal is absent patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.STOP_LOSS.value + assert trade.sell_reason == SellType.SELL_SIGNAL.value def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): From c69ce28b76449a1785cc41bc1491d66c766e8f9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:26:58 +0100 Subject: [PATCH 016/281] Update backtest assumption documentation --- docs/backtesting.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2121e3126..953198ddd 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -268,19 +268,24 @@ It contains some useful key metrics about performance of your strategy on backte Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Buys happen at open-price -- Sell signal sells happen at open-price of the following candle -- Low happens before high for stoploss, protecting capital first +- Sell-signal sells happen at open-price of the consecutive candle +- Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - ROI - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss sells happen exactly at stoploss price, even if low was lower +- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes +- Low happens before high for stoploss, protecting capital first - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) -- Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. +- Evaluation sequence (if multiple signals happen on the same candle) + - ROI (if not stoploss) + - Sell-signal + - Stoploss Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Also, keep in mind that past results don't guarantee future success. From 46ec6f498c7cf95c677955051ce17e3770cfb6ed Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 12:51:44 -0600 Subject: [PATCH 017/281] Correct link Fix prior redirection to a non-working link: https://www.freqtrade.io/en/latest/telegram-usage/configuration/#understand-forcebuy_enable --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 09cf21223..f4bd0a12a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md/#understand-forcebuy_enable) +[More details](configuration.md#understand-forcebuy_enable) ### /performance From 95c3c45ec95c3c4daa4c190f62bff3f867d0375b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 20:24:04 +0100 Subject: [PATCH 018/281] Remove long deprecated settings that moved from experimental to ask_strategy --- .../configuration/deprecated_settings.py | 30 ++++++++--- freqtrade/constants.py | 3 -- tests/test_configuration.py | 50 +++++++++++++++++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 03ed41ab8..6873ab405 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -26,6 +26,24 @@ def check_conflicting_settings(config: Dict[str, Any], ) +def process_removed_setting(config: Dict[str, Any], + section1: str, name1: str, + section2: str, name2: str) -> None: + """ + :param section1: Removed section + :param name1: Removed setting name + :param section2: new section for this key + :param name2: new setting name + """ + section1_config = config.get(section1, {}) + if name1 in section1_config: + raise OperationalException( + f"Setting `{section1}.{name1}` has been moved to `{section2}.{name2}. " + f"Please delete it from your configuration and use the `{section2}.{name2}` " + "setting instead." + ) + + def process_deprecated_setting(config: Dict[str, Any], section1: str, name1: str, section2: str, name2: str) -> None: @@ -51,12 +69,12 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal', 'experimental', 'ignore_roi_if_buy_signal') - process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal', - 'experimental', 'use_sell_signal') - process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only', - 'experimental', 'sell_profit_only') - process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', - 'experimental', 'ignore_roi_if_buy_signal') + process_removed_setting(config, 'experimental', 'use_sell_signal', + 'ask_strategy', 'use_sell_signal') + process_removed_setting(config, 'experimental', 'sell_profit_only', + 'ask_strategy', 'sell_profit_only') + process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal', + 'ask_strategy', 'ignore_roi_if_buy_signal') if (config.get('edge', {}).get('enabled', False) and 'capital_available_percentage' in config.get('edge', {})): diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2022556d2..3e523a49e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -182,9 +182,6 @@ CONF_SCHEMA = { 'experimental': { 'type': 'object', 'properties': { - 'use_sell_signal': {'type': 'boolean'}, - 'sell_profit_only': {'type': 'boolean'}, - 'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'block_bad_exchanges': {'type': 'boolean'} } }, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e6c91a96e..6c895a00b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -16,6 +16,7 @@ from freqtrade.configuration import (Configuration, check_exchange, remove_crede from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, process_deprecated_setting, + process_removed_setting, process_temporary_deprecated_settings) from freqtrade.configuration.load_config import load_config_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL @@ -1061,13 +1062,11 @@ def test_pairlist_resolving_fallback(mocker): assert config['datadir'] == Path.cwd() / "user_data/data/binance" +@pytest.mark.skip(reason='Currently no deprecated / moved sections') +# The below is kept as a sample for the future. @pytest.mark.parametrize("setting", [ ("ask_strategy", "use_sell_signal", True, "experimental", "use_sell_signal", False), - ("ask_strategy", "sell_profit_only", False, - "experimental", "sell_profit_only", True), - ("ask_strategy", "ignore_roi_if_buy_signal", False, - "experimental", "ignore_roi_if_buy_signal", True), ]) def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog): patched_configuration_load_config_file(mocker, default_conf) @@ -1097,6 +1096,25 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca assert default_conf[setting[0]][setting[1]] == setting[5] +@pytest.mark.parametrize("setting", [ + ("experimental", "use_sell_signal", False), + ("experimental", "sell_profit_only", True), + ("experimental", "ignore_roi_if_buy_signal", True), + ]) +def test_process_removed_settings(mocker, default_conf, setting, caplog): + patched_configuration_load_config_file(mocker, default_conf) + + # Create sections for new and deprecated settings + # (they may not exist in the config) + default_conf[setting[0]] = {} + # Assign removed setting + default_conf[setting[0]][setting[1]] = setting[2] + + # New and deprecated settings are conflicting ones + with pytest.raises(OperationalException, + match=r'Setting .* has been moved'): + process_temporary_deprecated_settings(default_conf) + def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): patched_configuration_load_config_file(mocker, edge_conf) edge_conf.update({'edge': { @@ -1196,6 +1214,30 @@ def test_process_deprecated_setting(mocker, default_conf, caplog): assert default_conf['sectionA']['new_setting'] == 'valA' +def test_process_removed_setting(mocker, default_conf, caplog): + patched_configuration_load_config_file(mocker, default_conf) + + # Create sections for new and deprecated settings + # (they may not exist in the config) + default_conf['sectionA'] = {} + default_conf['sectionB'] = {} + # Assign new setting + default_conf['sectionB']['somesetting'] = 'valA' + + # Only new setting exists (nothing should happen) + process_removed_setting(default_conf, + 'sectionA', 'somesetting', + 'sectionB', 'somesetting') + # Assign removed setting + default_conf['sectionA']['somesetting'] = 'valB' + + with pytest.raises(OperationalException, + match=r"Setting .* has been moved"): + process_removed_setting(default_conf, + 'sectionA', 'somesetting', + 'sectionB', 'somesetting') + + def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval." config = deepcopy(default_conf) From af1b3721fb736409cb8107912b664b07d4d7be30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 20:28:17 +0100 Subject: [PATCH 019/281] remove duplicate settings check --- freqtrade/configuration/deprecated_settings.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 6873ab405..6b2a20c8c 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -62,12 +62,11 @@ def process_deprecated_setting(config: Dict[str, Any], def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: - check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', - 'experimental', 'use_sell_signal') - check_conflicting_settings(config, 'ask_strategy', 'sell_profit_only', - 'experimental', 'sell_profit_only') - check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal', - 'experimental', 'ignore_roi_if_buy_signal') + # Kept for future deprecated / moved settings + # check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', + # 'experimental', 'use_sell_signal') + # process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal', + # 'experimental', 'use_sell_signal') process_removed_setting(config, 'experimental', 'use_sell_signal', 'ask_strategy', 'use_sell_signal') From 89573348b6ed83ac2e8f16f0888d41c767f1f337 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 20:37:52 -0600 Subject: [PATCH 020/281] Fix link --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 09cf21223..f4bd0a12a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md/#understand-forcebuy_enable) +[More details](configuration.md#understand-forcebuy_enable) ### /performance From 7cbd89657f1f477451d91c962f1fad260858385c Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 21:24:40 -0600 Subject: [PATCH 021/281] Initial step towards implementing proposed code --- freqtrade/constants.py | 2 +- freqtrade/pairlist/PerformanceFilter.py | 61 +++++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 22 ++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 freqtrade/pairlist/PerformanceFilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3271dda39..f47301fa6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -25,7 +25,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'ShuffleFilter', 'SpreadFilter'] + 'ShuffleFilter', 'SpreadFilter', 'PerformanceFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py new file mode 100644 index 000000000..e689ba0bc --- /dev/null +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -0,0 +1,61 @@ +""" +Performance pair list filter +""" +import logging +import random +from typing import Any, Dict, List + +import pandas as pd +from pandas import DataFrame, Series + +from freqtrade.pairlist.IPairList import IPairList + +from freqtrade.persistence import Trade +from datetime import timedelta, datetime, timezone + +logger = logging.getLogger(__name__) + +class PerformanceFilter(IPairList): + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return f"{self.name} - Sorting pairs by performance." + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ + + # Get the trading performance for pairs from database + perf = pd.DataFrame(Trade.get_overall_performance()) + # update pairlist with values from performance dataframe + # set initial value for pairs with no trades to 0 + # and sort the list using performance and count + list_df = pd.DataFrame({'pair':pairlist}) + sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ + .fillna(0).sort_values(by=['profit', 'count'], ascending=False) + pairlist = sorted_df['pair'].tolist() + + + return pairlist \ No newline at end of file diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f05bef1e..2643a0bd8 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -246,7 +246,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.03}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, - {"method": "ShuffleFilter"}], + {"method": "ShuffleFilter"}, {"method": "PerformanceFilter"}], "ETH", []), # AgeFilter and VolumePairList (require 2 days only, all should pass age test) ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, @@ -302,6 +302,18 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + # PerformanceFilter + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PerformanceFilter", "seed": 77}], + "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), + # PerformanceFilter, other seed + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PerformanceFilter", "seed": 42}], + "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), + # PerformanceFilter, no seed + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PerformanceFilter"}], + "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -326,6 +338,13 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected + # PrecisionFilter after StaticPairList + ([{"method": "StaticPairList"}, + {"method": "PrecisionFilter", "seed": 42}], + "BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']), + # PrecisionFilter only + ([{"method": "PrecisionFilter", "seed": 42}], + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], @@ -379,6 +398,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert isinstance(whitelist, list) # Verify length of pairlist matches (used for ShuffleFilter without seed) + # TBD if this applies to PerformanceFilter if type(whitelist_result) is list: assert whitelist == whitelist_result else: From 05686998bbc497f56d7407cc96d713686c4f6d85 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 21:26:42 -0600 Subject: [PATCH 022/281] Add starter entry in documentation --- docs/includes/pairlists.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index e6a9fc1a8..f8b33b27d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -15,6 +15,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) * [`AgeFilter`](#agefilter) +* [`PerformanceFilter`](#performancefilter) * [`PrecisionFilter`](#precisionfilter) * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) @@ -73,6 +74,10 @@ be caught out buying before the pair has finished dropping in price. This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. +#### PerformanceFilter + +Lorem ipsum. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. From c34150552f348245cac68611a27d4b26eabc5f8a Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 21:36:55 -0600 Subject: [PATCH 023/281] Revert unrelated change --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f4bd0a12a..09cf21223 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md#understand-forcebuy_enable) +[More details](configuration.md/#understand-forcebuy_enable) ### /performance From 335735062835636d1bce627b9117030a5595f69c Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:00:36 -0600 Subject: [PATCH 024/281] Revert unintended change --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index ec2d27174..9b15c9685 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -242,4 +242,4 @@ If this file is inexistent, then you're probably on a different version of MacOS ----- Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). \ No newline at end of file +[Bot Configuration](configuration.md). From 380cca225239397f04d503fee36c189ee06014aa Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:00:48 -0600 Subject: [PATCH 025/281] Remove unused imports --- freqtrade/pairlist/PerformanceFilter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index e689ba0bc..a2f2eb489 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -2,16 +2,13 @@ Performance pair list filter """ import logging -import random from typing import Any, Dict, List import pandas as pd -from pandas import DataFrame, Series from freqtrade.pairlist.IPairList import IPairList from freqtrade.persistence import Trade -from datetime import timedelta, datetime, timezone logger = logging.getLogger(__name__) From afb795b6f53de9ec00ccdbe2b4b128659831ad81 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:08:23 -0600 Subject: [PATCH 026/281] Remove unnecessary test PerforamnceFilter doesn't use seeds, so no need to provide different ones. --- tests/pairlist/test_pairlist.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 2643a0bd8..64468fc05 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -302,14 +302,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist - # PerformanceFilter + # PerformanceFilter, unneeded seed provided ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter", "seed": 77}], "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), - # PerformanceFilter, other seed - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PerformanceFilter", "seed": 42}], - "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), # PerformanceFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], From 91b4c80d35611bbcf812a3a46dd5860e0ec949d2 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:18:49 -0600 Subject: [PATCH 027/281] Remove unused parameters --- freqtrade/pairlist/PerformanceFilter.py | 2 -- tests/pairlist/test_pairlist.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index a2f2eb489..d4bd5936d 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -43,7 +43,6 @@ class PerformanceFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new whitelist """ - # Get the trading performance for pairs from database perf = pd.DataFrame(Trade.get_overall_performance()) # update pairlist with values from performance dataframe @@ -53,6 +52,5 @@ class PerformanceFilter(IPairList): sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() - return pairlist \ No newline at end of file diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 64468fc05..9814aea3e 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -425,7 +425,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert not log_has(logmsg, caplog) -def test_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None: +def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] @@ -498,7 +498,7 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) -def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers): +def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) @@ -514,7 +514,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pa pairlist_handler._whitelist_for_active_markets(['ETH/BTC']) -def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): +def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -652,7 +652,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) -def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): +def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [] From 9538fa1d723cca40a862a265e24fc89e3f559c06 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 00:24:48 -0600 Subject: [PATCH 028/281] Tweak main parameterized block for PerformanceFilter Remove randomized exception that was geared toward ShuffleFilter. Remove case involvoing seed, also geared toward ShuffleFilter. Mock get_overall_performance(). --- tests/pairlist/test_pairlist.py | 46 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9814aea3e..71d65d236 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -302,14 +302,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist - # PerformanceFilter, unneeded seed provided - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PerformanceFilter", "seed": 77}], - "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), - # PerformanceFilter, no seed + # PerformanceFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], - "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -381,6 +377,11 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) + # Provide for PerformanceFilter's dependency + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=[{'pair':'ETH/BTC','profit':5,'count':3}]), + ) + # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': with pytest.raises(OperationalException, @@ -394,7 +395,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert isinstance(whitelist, list) # Verify length of pairlist matches (used for ShuffleFilter without seed) - # TBD if this applies to PerformanceFilter if type(whitelist_result) is list: assert whitelist == whitelist_result else: @@ -544,7 +544,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf -def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] @@ -559,7 +559,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] @@ -660,3 +660,31 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): with pytest.raises(OperationalException, match=r"No Pairlist Handlers defined"): get_patched_freqtradebot(mocker, whitelist_conf) + + +@pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ + # Happy path, descening order, all values filled + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), +]) +def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): + whitelist_conf['pairlists'] = pairlists + whitelist_conf['stake_currency'] = base_currency + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + markets=PropertyMock(return_value=markets) + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=overall_performance), + ) + freqtrade.pairlists.refresh_pairlist() + whitelist = freqtrade.pairlists.whitelist + assert whitelist == expected From 4600bb807c41782420806c62f819fa56b393c6b9 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 00:38:06 -0600 Subject: [PATCH 029/281] Existing tests pass. --- tests/pairlist/test_pairlist.py | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 71d65d236..86e4616e0 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -330,12 +330,12 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PrecisionFilter after StaticPairList + # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PrecisionFilter", "seed": 42}], - "BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']), - # PrecisionFilter only - ([{"method": "PrecisionFilter", "seed": 42}], + {"method": "PerformanceFilter", "seed": 42}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Order matches order of appearance in whitelist_conf > exchange > pair_whitelist + # PerformanceFilter only + ([{"method": "PerformanceFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, @@ -662,29 +662,29 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): get_patched_freqtradebot(mocker, whitelist_conf) -@pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ - # Happy path, descening order, all values filled - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), -]) -def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): - whitelist_conf['pairlists'] = pairlists - whitelist_conf['stake_currency'] = base_currency +# @pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ +# # Happy path, descening order, all values filled +# ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), +# ]) +# def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): +# whitelist_conf['pairlists'] = pairlists +# whitelist_conf['stake_currency'] = base_currency - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) +# mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', - get_tickers=tickers, - markets=PropertyMock(return_value=markets) - ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) +# freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) +# mocker.patch.multiple('freqtrade.exchange.Exchange', +# get_tickers=tickers, +# markets=PropertyMock(return_value=markets) +# ) +# mocker.patch.multiple( +# 'freqtrade.exchange.Exchange', +# get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), +# ) - mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=overall_performance), - ) - freqtrade.pairlists.refresh_pairlist() - whitelist = freqtrade.pairlists.whitelist - assert whitelist == expected +# mocker.patch.multiple('freqtrade.persistence.Trade', +# get_overall_performance=MagicMock(return_value=overall_performance), +# ) +# freqtrade.pairlists.refresh_pairlist() +# whitelist = freqtrade.pairlists.whitelist +# assert whitelist == expected From 26855800a3b0229c1ea3a3c0f9d97268e9de57f7 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 00:39:18 -0600 Subject: [PATCH 030/281] Remove unused seed --- tests/pairlist/test_pairlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 86e4616e0..ae80a3975 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -332,10 +332,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'filter_at_the_beginning'), # OperationalException expected # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PerformanceFilter", "seed": 42}], + {"method": "PerformanceFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Order matches order of appearance in whitelist_conf > exchange > pair_whitelist # PerformanceFilter only - ([{"method": "PerformanceFilter", "seed": 42}], + ([{"method": "PerformanceFilter"}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, From 662ec3207310dd0c269e1a5bc4554eefec1891a6 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:15:36 -0600 Subject: [PATCH 031/281] Add test cases --- freqtrade/pairlist/PerformanceFilter.py | 4 +- tests/pairlist/test_pairlist.py | 65 ++++++++++++++++--------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index d4bd5936d..b2889dc6b 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -45,10 +45,10 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database perf = pd.DataFrame(Trade.get_overall_performance()) - # update pairlist with values from performance dataframe + # get pairlist from performance dataframe values + list_df = pd.DataFrame({'pair':pairlist}) # set initial value for pairs with no trades to 0 # and sort the list using performance and count - list_df = pd.DataFrame({'pair':pairlist}) sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index ae80a3975..a99651727 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -662,29 +662,48 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): get_patched_freqtradebot(mocker, whitelist_conf) -# @pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ -# # Happy path, descening order, all values filled -# ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), -# ]) -# def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): -# whitelist_conf['pairlists'] = pairlists -# whitelist_conf['stake_currency'] = base_currency +@pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ + # Happy path, descending order, all values filled + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC'], + [{'pair':'TKN/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], + ['TKN/BTC','ETH/BTC']), + # Performance data outside allow list ignored + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC'], + [{'pair':'OTHER/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], + ['ETH/BTC','TKN/BTC']), + # Partial performance data missing and sorted between positive and negative profit + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC','LTC/BTC'], + [{'pair':'ETH/BTC','profit':-5,'count':100}, {'pair':'TKN/BTC','profit':4,'count':2}], + ['TKN/BTC','LTC/BTC','ETH/BTC']), + # Tie in performance data broken by count + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC','LTC/BTC'], + [{'pair':'LTC/BTC','profit':-5,'count':101}, {'pair':'TKN/BTC','profit':-5,'count':2}, {'pair':'ETH/BTC','profit':-5,'count':100}, ], + ['LTC/BTC','ETH/BTC','TKN/BTC']), +]) +def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): + allowlist_conf = whitelist_conf + allowlist_conf['pairlists'] = pairlists + allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist -# mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) -# freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) -# mocker.patch.multiple('freqtrade.exchange.Exchange', -# get_tickers=tickers, -# markets=PropertyMock(return_value=markets) -# ) -# mocker.patch.multiple( -# 'freqtrade.exchange.Exchange', -# get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), -# ) + freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + markets=PropertyMock(return_value=markets) + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) -# mocker.patch.multiple('freqtrade.persistence.Trade', -# get_overall_performance=MagicMock(return_value=overall_performance), -# ) -# freqtrade.pairlists.refresh_pairlist() -# whitelist = freqtrade.pairlists.whitelist -# assert whitelist == expected + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=overall_performance), + ) + freqtrade.pairlists.refresh_pairlist() + allowlist = freqtrade.pairlists.whitelist + assert allowlist == allowlist_result From dbd50fdff64e5e72a052db687082b26794790f7b Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:22:03 -0600 Subject: [PATCH 032/281] Document filter. --- docs/includes/pairlists.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index f8b33b27d..50ef52653 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -76,7 +76,12 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter -Lorem ipsum. +Sorts pairs by performance, as follows: +1. Positive performance. +2. No closed trades yet. +3. Negative performance. + +Trade count is used as a tie breaker. #### PrecisionFilter From 966c6b308f182392c85a74568350de4ac5cd9ced Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:34:18 -0600 Subject: [PATCH 033/281] Satisfy linter. --- freqtrade/pairlist/PerformanceFilter.py | 6 +-- tests/pairlist/test_pairlist.py | 51 +++++++++++++------------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index b2889dc6b..099b8d271 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -12,6 +12,7 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) + class PerformanceFilter(IPairList): def __init__(self, exchange, pairlistmanager, @@ -19,7 +20,6 @@ class PerformanceFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - @property def needstickers(self) -> bool: """ @@ -46,11 +46,11 @@ class PerformanceFilter(IPairList): # Get the trading performance for pairs from database perf = pd.DataFrame(Trade.get_overall_performance()) # get pairlist from performance dataframe values - list_df = pd.DataFrame({'pair':pairlist}) + list_df = pd.DataFrame({'pair': pairlist}) # set initial value for pairs with no trades to 0 # and sort the list using performance and count sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() - return pairlist \ No newline at end of file + return pairlist diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 65a0fa835..9e2bab12c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -729,27 +729,32 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): @pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ # Happy path, descending order, all values filled - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC'], - [{'pair':'TKN/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], - ['TKN/BTC','ETH/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC'], + [{'pair': 'TKN/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], + ['TKN/BTC', 'ETH/BTC']), # Performance data outside allow list ignored - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC'], - [{'pair':'OTHER/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], - ['ETH/BTC','TKN/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC'], + [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, + {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], + ['ETH/BTC', 'TKN/BTC']), # Partial performance data missing and sorted between positive and negative profit - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC','LTC/BTC'], - [{'pair':'ETH/BTC','profit':-5,'count':100}, {'pair':'TKN/BTC','profit':4,'count':2}], - ['TKN/BTC','LTC/BTC','ETH/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'ETH/BTC', 'profit': -5, 'count': 100}, + {'pair': 'TKN/BTC', 'profit': 4, 'count': 2}], + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), # Tie in performance data broken by count - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC','LTC/BTC'], - [{'pair':'LTC/BTC','profit':-5,'count':101}, {'pair':'TKN/BTC','profit':-5,'count':2}, {'pair':'ETH/BTC','profit':-5,'count':100}, ], - ['LTC/BTC','ETH/BTC','TKN/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'LTC/BTC', 'profit': -5, 'count': 101}, + {'pair': 'TKN/BTC', 'profit': -5, 'count': 2}, + {'pair': 'ETH/BTC', 'profit': -5, 'count': 100}], + ['LTC/BTC', 'ETH/BTC', 'TKN/BTC']), ]) -def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): +def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, + allowlist_result, tickers, markets, ohlcv_history_list): allowlist_conf = whitelist_conf allowlist_conf['pairlists'] = pairlists allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist @@ -761,14 +766,12 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o get_tickers=tickers, markets=PropertyMock(return_value=markets) ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) - + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=overall_performance), - ) + get_overall_performance=MagicMock(return_value=overall_performance), + ) freqtrade.pairlists.refresh_pairlist() allowlist = freqtrade.pairlists.whitelist assert allowlist == allowlist_result From fefa500963d7d4a8b88d740783190755605942ac Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:34:40 -0600 Subject: [PATCH 034/281] More lint --- tests/pairlist/test_pairlist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9e2bab12c..5e9847e3d 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -333,7 +333,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Order matches order of appearance in whitelist_conf > exchange > pair_whitelist + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # PerformanceFilter only ([{"method": "PerformanceFilter"}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -383,7 +383,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=[{'pair':'ETH/BTC','profit':5,'count':3}]), + get_overall_performance=MagicMock( + return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count' :3}]), ) # Set whitelist_result to None if pairlist is invalid and should produce exception From ecce5265f5e5fa153261095c563e505c243fc0a7 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:43:19 -0600 Subject: [PATCH 035/281] Linting --- tests/pairlist/test_pairlist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5e9847e3d..a4df031c9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -383,8 +383,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock( - return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count' :3}]), + get_overall_performance=MagicMock(return_value=\ + [{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), ) # Set whitelist_result to None if pairlist is invalid and should produce exception @@ -737,7 +737,7 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): # Performance data outside allow list ignored ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC'], - [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, + [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], ['ETH/BTC', 'TKN/BTC']), # Partial performance data missing and sorted between positive and negative profit @@ -769,7 +769,7 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o ) mocker.patch.multiple('freqtrade.exchange.Exchange', get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) + ) mocker.patch.multiple('freqtrade.persistence.Trade', get_overall_performance=MagicMock(return_value=overall_performance), ) From f448564073b2d7c487ccb2d75e749bfe949bd547 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:49:46 -0600 Subject: [PATCH 036/281] Lint --- tests/pairlist/test_pairlist.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a4df031c9..d40cece41 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -383,9 +383,9 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=\ - [{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), - ) + get_overall_performance=MagicMock( + return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), + ) # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': From 37d2e476df19bfafa86c6405404d0bc578f270d9 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:59:30 -0600 Subject: [PATCH 037/281] isort imports --- freqtrade/pairlist/PerformanceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index 099b8d271..bd56a4607 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -7,9 +7,9 @@ from typing import Any, Dict, List import pandas as pd from freqtrade.pairlist.IPairList import IPairList - from freqtrade.persistence import Trade + logger = logging.getLogger(__name__) From 4cb331b5ad644f9d1aeee2909e158de25078a913 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 10:24:44 +0100 Subject: [PATCH 038/281] Remove non-needed parameters from tests --- tests/test_configuration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6c895a00b..167215f29 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1101,7 +1101,7 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca ("experimental", "sell_profit_only", True), ("experimental", "ignore_roi_if_buy_signal", True), ]) -def test_process_removed_settings(mocker, default_conf, setting, caplog): +def test_process_removed_settings(mocker, default_conf, setting): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings @@ -1115,7 +1115,8 @@ def test_process_removed_settings(mocker, default_conf, setting, caplog): match=r'Setting .* has been moved'): process_temporary_deprecated_settings(default_conf) -def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): + +def test_process_deprecated_setting_edge(mocker, edge_conf): patched_configuration_load_config_file(mocker, edge_conf) edge_conf.update({'edge': { 'enabled': True, From a47d8dbe56c1b3813a15b125726f7dc66a35666b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 11:31:28 +0100 Subject: [PATCH 039/281] Small refactor, avoiding duplicate calculation of profits --- freqtrade/optimize/optimize_reports.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index fc04cbd93..6aef031d3 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -58,16 +58,19 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: """ Generate one result dict, with "first_column" as key. """ + profit_sum = result['profit_percent'].sum() + profit_total = profit_sum / max_open_trades + return { 'key': first_column, 'trades': len(result), 'profit_mean': result['profit_percent'].mean() if len(result) > 0 else 0.0, 'profit_mean_pct': result['profit_percent'].mean() * 100.0 if len(result) > 0 else 0.0, - 'profit_sum': result['profit_percent'].sum(), - 'profit_sum_pct': result['profit_percent'].sum() * 100.0, + 'profit_sum': profit_sum, + 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': result['profit_percent'].sum() / max_open_trades, - 'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades, + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100.0, 2), 'duration_avg': str(timedelta( minutes=round(result['trade_duration'].mean())) ) if not result.empty else '0:00', @@ -122,8 +125,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List result = results.loc[results['sell_reason'] == reason] profit_mean = result['profit_percent'].mean() - profit_sum = result["profit_percent"].sum() - profit_percent_tot = result['profit_percent'].sum() / max_open_trades + profit_sum = result['profit_percent'].sum() + profit_total = profit_sum / max_open_trades tabular_data.append( { @@ -137,8 +140,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_percent_tot, - 'profit_total_pct': round(profit_percent_tot * 100, 2), + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100, 2), } ) return tabular_data From ff286bd80cd82fa079ec51cc0592ba2f986c2ceb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 16:32:44 +0100 Subject: [PATCH 040/281] Slightly clarify hyperopt docs --- docs/hyperopt.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index fc7a0dd93..c42889831 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -173,6 +173,11 @@ one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: ```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: conditions = [] # GUARDS AND TRENDS From 56529180eb6e73dfcac8ebcab5f35e4c731e2a20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 16:42:08 +0100 Subject: [PATCH 041/281] Further improve hyperopt docs --- docs/hyperopt.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index c42889831..f88d9cd4f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -64,9 +64,9 @@ Depending on the space you want to optimize, only some of the below are required Optional in hyperopt - can also be loaded from a strategy (recommended): -* copy `populate_indicators` from your strategy - otherwise default-strategy will be used -* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used -* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy !!! Note You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. @@ -104,7 +104,7 @@ This command will create a new hyperopt file from a template, allowing you to ge There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: * Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Inside `populate_buy_trend()` - applying the parameters. +* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. There you have two different types of indicators: 1. `guards` and 2. `triggers`. @@ -128,7 +128,7 @@ Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods * Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Inside `populate_sell_trend()` - applying the parameters. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. The configuration and rules are the same than for buy signals. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. From e1d42ba78ce670a662420b7bb19337c57cc335ca Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 09:44:01 -0600 Subject: [PATCH 042/281] Alphabetize --- freqtrade/constants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 20cc70d2e..9d0078d21 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,9 +24,9 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', - 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', - 'PerformanceFilter'] + 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', + 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', + 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' From 03c5714399d57af12cb18ffa7f4b6937ed3cd6b4 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 09:45:17 -0600 Subject: [PATCH 043/281] Use explicit merge without depending on library detail. Add no trades case. --- freqtrade/pairlist/PerformanceFilter.py | 9 +++++++-- tests/pairlist/test_pairlist.py | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index bd56a4607..2d360a346 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -44,12 +44,17 @@ class PerformanceFilter(IPairList): :return: new whitelist """ # Get the trading performance for pairs from database - perf = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance()) + + # Skip performance-based sorting if no performance data is available + if len(performance) == 0: + return pairlist + # get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) # set initial value for pairs with no trades to 0 # and sort the list using performance and count - sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ + sorted_df = list_df.merge(performance, on='pair', how='left')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index d40cece41..c62ec81f3 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -383,8 +383,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock( - return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), + get_overall_performance=MagicMock(return_value=[]) ) # Set whitelist_result to None if pairlist is invalid and should produce exception @@ -729,7 +728,10 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): @pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ - # Happy path, descending order, all values filled + # No trades yet + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], [], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # Happy path: Descending order, all values filled ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC'], [{'pair': 'TKN/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], From a00f852cf99106b314a05d425b7ab6fcad9d158d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 16:56:08 +0100 Subject: [PATCH 044/281] Add best / worst pair to summary statistics --- docs/backtesting.md | 12 ++++++------ freqtrade/optimize/optimize_reports.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 277b11083..01624e5c2 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -165,8 +165,8 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | | Best day | 25.27% | @@ -238,8 +238,8 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | | Best day | 25.27% | @@ -258,8 +258,8 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. - `Total trades`: Identical to the total trades of the backtest output table. -- `First trade`: First trade entered. -- `First trade pair`: Which pair was part of the first trade. +- `Best Pair`: Which pair performed best, and it's corresponding `Cum Profit %`. +- `Worst pair`: Which pair performed worst and it's corresponding `Cum Profit %`. - `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best day` / `Worst day`: Best and worst day based on daily profit. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6aef031d3..589e0ba1c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -256,13 +256,18 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], results=results.loc[results['open_at_end']], 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'), + '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, @@ -395,17 +400,19 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: - min_trade = min(strat_results['trades'], key=lambda x: x['open_date']) metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), - ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), - ('First trade Pair', min_trade['pair']), ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('', ''), # Empty line to improve readability + ('Best Pair', f"{strat_results['best_pair']['key']} - " + f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), + ('Worst Pair', f"{strat_results['worst_pair']['key']} - " + f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Days win/draw/lose', f"{strat_results['winning_days']} / " From 5d3f59df90d97f1d96e7b4e9734dcc722b6bc7a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 17:45:56 +0100 Subject: [PATCH 045/281] Add best / worst trade --- docs/backtesting.md | 18 ++++++++++++------ freqtrade/optimize/optimize_reports.py | 5 +++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 01624e5c2..42de9bdc3 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -165,10 +165,13 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | +| | | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | +| Best Trade | LSK/BTC - 4.25% | +| Worst Trade | ZEC/BTC - -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -238,10 +241,13 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | +| | | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | +| Best Trade | LSK/BTC - 4.25% | +| Worst Trade | ZEC/BTC - -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -258,10 +264,10 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. - `Total trades`: Identical to the total trades of the backtest output table. -- `Best Pair`: Which pair performed best, and it's corresponding `Cum Profit %`. -- `Worst pair`: Which pair performed worst and it's corresponding `Cum Profit %`. - `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. +- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 589e0ba1c..3e44a6067 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -400,6 +400,8 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: 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_percent']) + worst_trade = min(strat_results['trades'], key=lambda x: x['profit_percent']) metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), @@ -413,6 +415,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), ('Worst Pair', f"{strat_results['worst_pair']['key']} - " f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), + ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"), + ('Worst trade', f"{worst_trade['pair']} {round(worst_trade['profit_percent'] * 100, 2)}%"), + ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Days win/draw/lose', f"{strat_results['winning_days']} / " From e40d97e05e765c5946208b47b5519c47b79aa135 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 17:52:29 +0100 Subject: [PATCH 046/281] Small formatting improvements --- docs/backtesting.md | 16 ++++++++-------- freqtrade/optimize/optimize_reports.py | 7 ++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 42de9bdc3..c841899a7 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -168,10 +168,10 @@ A backtesting result will look like that: | Total Profit % | 152.41% | | Trades per day | 3.575 | | | | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | -| Best Trade | LSK/BTC - 4.25% | -| Worst Trade | ZEC/BTC - -10.25% | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -244,10 +244,10 @@ It contains some useful key metrics about performance of your strategy on backte | Total Profit % | 152.41% | | Trades per day | 3.575 | | | | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | -| Best Trade | LSK/BTC - 4.25% | -| Worst Trade | ZEC/BTC - -10.25% | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3e44a6067..b3799856e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -411,12 +411,13 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), ('', ''), # Empty line to improve readability - ('Best Pair', f"{strat_results['best_pair']['key']} - " + ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), - ('Worst Pair', f"{strat_results['worst_pair']['key']} - " + ('Worst Pair', f"{strat_results['worst_pair']['key']} " f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"), - ('Worst trade', f"{worst_trade['pair']} {round(worst_trade['profit_percent'] * 100, 2)}%"), + ('Worst trade', f"{worst_trade['pair']} " + f"{round(worst_trade['profit_percent'] * 100, 2)}%"), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), From 6a74c57c3d5c4ab422ced69643459c70269981ab Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 11:33:25 -0600 Subject: [PATCH 047/281] Pair name-based sorting. Attempt at more rational string sorting. Change test to show not working as expected. --- freqtrade/pairlist/PerformanceFilter.py | 2 +- tests/pairlist/test_pairlist.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index 2d360a346..5e1ec3c66 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -55,7 +55,7 @@ class PerformanceFilter(IPairList): # set initial value for pairs with no trades to 0 # and sort the list using performance and count sorted_df = list_df.merge(performance, on='pair', how='left')\ - .fillna(0).sort_values(by=['profit', 'count'], ascending=False) + .fillna(0).sort_values(by=['profit', 'count', 'pair'], ascending=False) pairlist = sorted_df['pair'].tolist() return pairlist diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index c62ec81f3..475691327 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -305,7 +305,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # PerformanceFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ADAHALF/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected From 323c0657f8a80b0af14d0ff18920f431f66af7a0 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:17:03 -0600 Subject: [PATCH 048/281] Sort by profit after sort by count/pair --- freqtrade/pairlist/PerformanceFilter.py | 21 +++++++++++++-------- tests/pairlist/test_pairlist.py | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index 5e1ec3c66..cdc3c78ad 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -31,17 +31,17 @@ class PerformanceFilter(IPairList): def short_desc(self) -> str: """ - Short whitelist method description - used for startup-messages + Short allowlist method description - used for startup-messages """ return f"{self.name} - Sorting pairs by performance." def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ - Filters and sorts pairlist and returns the whitelist again. + Filters and sorts pairlist and returns the allowlist again. Called on each bot iteration - please use internal caching if necessary :param pairlist: pairlist to filter or sort :param tickers: Tickers (from exchange.get_tickers()). May be cached. - :return: new whitelist + :return: new allowlist """ # Get the trading performance for pairs from database performance = pd.DataFrame(Trade.get_overall_performance()) @@ -49,13 +49,18 @@ class PerformanceFilter(IPairList): # Skip performance-based sorting if no performance data is available if len(performance) == 0: return pairlist - - # get pairlist from performance dataframe values + + # Get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) - # set initial value for pairs with no trades to 0 - # and sort the list using performance and count + + # Set initial value for pairs with no trades to 0 + # Sort the list using: + # - primarily performance (high to low) + # - then count (low to high, so as to favor same performance with fewer trades) + # - then pair name alphametically sorted_df = list_df.merge(performance, on='pair', how='left')\ - .fillna(0).sort_values(by=['profit', 'count', 'pair'], ascending=False) + .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ + .sort_values(by=['profit'], ascending=False) pairlist = sorted_df['pair'].tolist() return pairlist diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 475691327..244f92d8b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -748,13 +748,20 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): [{'pair': 'ETH/BTC', 'profit': -5, 'count': 100}, {'pair': 'TKN/BTC', 'profit': 4, 'count': 2}], ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), - # Tie in performance data broken by count + # Tie in performance data broken by count (ascending) ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], - [{'pair': 'LTC/BTC', 'profit': -5, 'count': 101}, - {'pair': 'TKN/BTC', 'profit': -5, 'count': 2}, - {'pair': 'ETH/BTC', 'profit': -5, 'count': 100}], - ['LTC/BTC', 'ETH/BTC', 'TKN/BTC']), + [{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 101}, + {'pair': 'TKN/BTC', 'profit': -5.01, 'count': 2}, + {'pair': 'ETH/BTC', 'profit': -5.01, 'count': 100}], + ['TKN/BTC', 'ETH/BTC', 'LTC/BTC']), + # Tie in performance and count, broken by alphabetical sort + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 1}, + {'pair': 'TKN/BTC', 'profit': -5.01, 'count': 1}, + {'pair': 'ETH/BTC', 'profit': -5.01, 'count': 1}], + ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), ]) def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): From d6c93919246a7e1e250f2934b3da7417a292118b Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:18:23 -0600 Subject: [PATCH 049/281] Restoring expectation --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 244f92d8b..4b4f51b37 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -305,7 +305,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # PerformanceFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ADAHALF/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected From e7a035eefe3bdcaeca5688051b778f7b48788505 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:29:31 -0600 Subject: [PATCH 050/281] Lint --- freqtrade/pairlist/PerformanceFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index cdc3c78ad..92a97099e 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -49,7 +49,7 @@ class PerformanceFilter(IPairList): # Skip performance-based sorting if no performance data is available if len(performance) == 0: return pairlist - + # Get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) @@ -60,7 +60,7 @@ class PerformanceFilter(IPairList): # - then pair name alphametically sorted_df = list_df.merge(performance, on='pair', how='left')\ .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ - .sort_values(by=['profit'], ascending=False) + .sort_values(by=['profit'], ascending=False) pairlist = sorted_df['pair'].tolist() return pairlist From 4b6f5b92b59e21896035c2f47475a3fcdf07c377 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:47:36 -0600 Subject: [PATCH 051/281] Remove non-pertinent test case --- tests/pairlist/test_pairlist.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 4b4f51b37..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -302,10 +302,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist - # PerformanceFilter - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PerformanceFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected From 1791495475e1e0bbab0b820a065b627b92f28f7d Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 16:50:44 -0600 Subject: [PATCH 052/281] Trigger another run of tests --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1d2f16b45..884be3c24 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -332,7 +332,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # PerformanceFilter only ([{"method": "PerformanceFilter"}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], From 90070f0dc54cd2ea92268ce75240ff3972523666 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 17:17:40 -0600 Subject: [PATCH 053/281] Force test rerun --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 884be3c24..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -332,7 +332,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # PerformanceFilter only ([{"method": "PerformanceFilter"}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], From 5f8e67d2b25c40454343414653e02b48fd4518d8 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sun, 29 Nov 2020 05:05:54 -0600 Subject: [PATCH 054/281] Update docs/includes/pairlists.md Co-authored-by: Matthias --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index a1bbebbf7..844f1d70a 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -77,7 +77,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter -Sorts pairs by performance, as follows: +Sorts pairs by past trade performance, as follows: 1. Positive performance. 2. No closed trades yet. 3. Negative performance. From 99abe52043ab7b7f54e79fd75c1c80831eafebf5 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sun, 29 Nov 2020 10:30:02 -0600 Subject: [PATCH 055/281] Trigger CI --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1d2f16b45..1f434ae34 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -326,7 +326,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PerformanceFilter after StaticPairList + # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), From b7de18608d8f977578e56d3c83b88fbbbb459f3e Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sun, 29 Nov 2020 10:30:43 -0600 Subject: [PATCH 056/281] Trigger CI --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f434ae34..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -326,7 +326,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PerformanceFilter after StaticPairList + # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), From f17c7f0609f66f33b4e14fc79c9f56fd6665e394 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 05:42:00 +0000 Subject: [PATCH 057/281] Bump plotly from 4.12.0 to 4.13.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.12.0 to 4.13.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.12.0...v4.13.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index bd40bc0b5..1c3b03133 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.12.0 +plotly==4.13.0 From 275cfb3a9c8bfa69be15a14b3b38252df86f061a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 05:42:12 +0000 Subject: [PATCH 058/281] Bump ccxt from 1.38.13 to 1.38.55 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.38.13 to 1.38.55. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.38.13...1.38.55) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7490688d4..f72e30480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.38.13 +ccxt==1.38.55 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 14d44b2cd6b8c9dc5884ee61b3432a0df43d131b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 08:02:09 +0000 Subject: [PATCH 059/281] Bump python-telegram-bot from 13.0 to 13.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.0 to 13.1. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.0...v13.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f72e30480..f59754f93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.1.4 ccxt==1.38.55 aiohttp==3.7.3 SQLAlchemy==1.3.20 -python-telegram-bot==13.0 +python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.1.1 requests==2.25.0 From 202ca88e2311bda492757351cef667630fa498de Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 17:37:19 +0100 Subject: [PATCH 060/281] Changes to pi steup --- docs/installation.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 9b15c9685..b6197c905 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -105,13 +105,17 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` === "RaspberryPi/Raspbian" - The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/). This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. + ``` bash sudo apt-get install python3-venv libatlas-base-dev + # Use pywheels.org to speed up installation + sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > sudo tee /etc/pip.conf + git clone https://github.com/freqtrade/freqtrade.git cd freqtrade @@ -120,6 +124,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note "Installation duration" Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. + Due to this, we recommend to use the prebuild docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md) !!! Note The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. From 95b24ba8a931ab8a7d440548f820a40342fb694e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 20:56:14 +0100 Subject: [PATCH 061/281] Update setup.sh with some specifics --- docs/installation.md | 4 ++-- setup.sh | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index b6197c905..4a2450ea2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -90,13 +90,13 @@ Each time you open a new terminal, you must run `source .env/bin/activate`. ## Custom Installation -We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. +We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -=== "Ubuntu 16.04" +=== "Ubuntu/Debian" #### Install necessary dependencies ```bash diff --git a/setup.sh b/setup.sh index 049a6a77e..83ba42d9b 100755 --- a/setup.sh +++ b/setup.sh @@ -61,13 +61,25 @@ function updateenv() { read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - ${PYTHON} -m pip install --upgrade -r requirements-dev.txt + REQUIREMENTS=requirements-dev.txt else - ${PYTHON} -m pip install --upgrade -r requirements.txt - echo "Dev dependencies ignored." + REQUIREMENTS=requirements.txt + fi + SYS_ARCH=$(uname -m) + if [ "${SYS_ARCH}" == "armv7l" ]; then + echo "Detected Raspberry, installing cython." + ${PYTHON} -m pip install --upgrade cython + fi + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} + if [ $? -ne 0 ]; then + echo "Failed installing dependencies" + exit 1 fi - ${PYTHON} -m pip install -e . + if [ $? -ne 0 ]; then + echo "Failed installing Freqtrade" + exit 1 + fi echo "pip install completed" echo } @@ -134,11 +146,11 @@ function reset() { git fetch -a - if [ "1" == $(git branch -vv |grep -c "* develop") ] + if [ "1" == $(git branch -vv | grep -c "* develop") ] then echo "- Hard resetting of 'develop' branch." git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* stable") ] + elif [ "1" == $(git branch -vv | grep -c "* stable") ] then echo "- Hard resetting of 'stable' branch." git reset --hard origin/stable @@ -149,7 +161,7 @@ function reset() { fi if [ -d ".env" ]; then - echo "- Delete your previous virtual env" + echo "- Deleting your previous virtual env" rm -rf .env fi echo @@ -253,7 +265,7 @@ function install() { echo "Run the bot !" echo "-------------------------" echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade '." - echo "You can see the list of available bot subcommands by executing 'source .env/bin/activate; freqtrade --help'." + echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'." echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'." } From 5f70d1f9a7bc58969cee2c3fa40b981047d22a9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 21:10:11 +0100 Subject: [PATCH 062/281] Ask for hyperopt installation during setup closes #2871 --- docs/installation.md | 2 +- setup.sh | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 4a2450ea2..5cc0e03f4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -114,7 +114,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` bash sudo apt-get install python3-venv libatlas-base-dev # Use pywheels.org to speed up installation - sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > sudo tee /etc/pip.conf + sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf git clone https://github.com/freqtrade/freqtrade.git cd freqtrade diff --git a/setup.sh b/setup.sh index 83ba42d9b..8896331e3 100755 --- a/setup.sh +++ b/setup.sh @@ -56,6 +56,7 @@ function updateenv() { exit 1 fi source .env/bin/activate + SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." ${PYTHON} -m pip install --upgrade pip read -p "Do you want to install dependencies for dev [y/N]? " @@ -65,12 +66,21 @@ function updateenv() { else REQUIREMENTS=requirements.txt fi - SYS_ARCH=$(uname -m) + REQUIREMENTS_HYPEROPT="" + if [ "${SYS_ARCH}" != "armv7l" ]; then + # Is not Raspberry + read -p "Do you want to install hyperopt dependencies for dev [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" + fi + fi + if [ "${SYS_ARCH}" == "armv7l" ]; then echo "Detected Raspberry, installing cython." ${PYTHON} -m pip install --upgrade cython fi - ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} if [ $? -ne 0 ]; then echo "Failed installing dependencies" exit 1 From cec771b59396658d7d89241e869da9459411a4b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 21:17:50 +0100 Subject: [PATCH 063/281] Ask for plotting dependency installation --- setup.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/setup.sh b/setup.sh index 8896331e3..af5e70691 100755 --- a/setup.sh +++ b/setup.sh @@ -67,20 +67,25 @@ function updateenv() { REQUIREMENTS=requirements.txt fi REQUIREMENTS_HYPEROPT="" - if [ "${SYS_ARCH}" != "armv7l" ]; then + REQUIREMENTS_PLOT="" + read -p "Do you want to install plotting dependencies (plotly) [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_PLOT="-r requirements-plot.txt" + fi + if [ "${SYS_ARCH}" == "armv7l" ]; then + echo "Detected Raspberry, installing cython, skipping hyperopt installation." + ${PYTHON} -m pip install --upgrade cython + else # Is not Raspberry - read -p "Do you want to install hyperopt dependencies for dev [y/N]? " + read -p "Do you want to install hyperopt dependencies [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" fi fi - if [ "${SYS_ARCH}" == "armv7l" ]; then - echo "Detected Raspberry, installing cython." - ${PYTHON} -m pip install --upgrade cython - fi - ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} if [ $? -ne 0 ]; then echo "Failed installing dependencies" exit 1 From 36b7edc342cac2680c7408eb17cf412b3416b863 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 19:55:20 +0100 Subject: [PATCH 064/281] Update typing errors --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 48 +++++++++++++++++++++---------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 384d7c6c2..8c2c203e6 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -470,7 +470,7 @@ class ApiServer(RPC): @require_login @rpc_catch_errors - def _trades_delete(self, tradeid): + def _trades_delete(self, tradeid: int): """ Handler for DELETE /trades/ endpoint. Removes the trade from the database (tries to cancel open orders first!) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e608a2274..9ac271ba0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -542,7 +542,7 @@ class RPC: else: return None - def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]: + def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: """ Handler for delete . Delete the given trade and close eventually existing open orders. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 31d5bbfbd..7239eab02 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,11 +5,11 @@ This module manage Telegram communication """ import json import logging -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -71,7 +71,7 @@ class Telegram(RPC): """ super().__init__(freqtrade) - self._updater: Updater = None + self._updater: Updater self._config = freqtrade.config self._init() if self._config.get('fiat_display_currency', None): @@ -231,7 +231,7 @@ class Telegram(RPC): :return: None """ - if 'table' in context.args: + if context.args and 'table' in context.args: self._status_table(update, context) return @@ -305,7 +305,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: - timescale = int(context.args[0]) + timescale = int(context.args[0]) if context.args else 0 except (TypeError, ValueError, IndexError): timescale = 7 try: @@ -485,7 +485,10 @@ class Telegram(RPC): :return: None """ - trade_id = context.args[0] if len(context.args) > 0 else None + trade_id = context.args[0] if context.args and len(context.args) > 0 else None + if not trade_id: + self._send_msg("You must specify a trade-id or 'all'.") + return try: msg = self._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) @@ -502,13 +505,13 @@ class Telegram(RPC): :param update: message update :return: None """ - - pair = context.args[0] - price = float(context.args[1]) if len(context.args) > 1 else None - try: - self._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + if context.args: + pair = context.args[0] + price = float(context.args[1]) if len(context.args) > 1 else None + try: + self._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -521,7 +524,7 @@ class Telegram(RPC): """ stake_cur = self._config['stake_currency'] try: - nrecent = int(context.args[0]) + nrecent = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): nrecent = 10 try: @@ -554,9 +557,10 @@ class Telegram(RPC): :param update: message update :return: None """ - - trade_id = context.args[0] if len(context.args) > 0 else None try: + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) msg = self._rpc_delete(trade_id) self._send_msg(( '`{result_msg}`\n' @@ -676,7 +680,7 @@ class Telegram(RPC): """ try: try: - limit = int(context.args[0]) + limit = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): limit = 10 logs = self._rpc_get_logs(limit)['logs'] @@ -802,7 +806,7 @@ class Telegram(RPC): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN, + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False) -> None: """ Send given markdown message @@ -812,9 +816,11 @@ class Telegram(RPC): :return: None """ - keyboard = [['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help']] + keyboard: List[List[Union[str, KeyboardButton]]] = [ + ['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help'] + ] reply_markup = ReplyKeyboardMarkup(keyboard) From 5dfa1807a3c99499f3304d6046eeed96f8cc6825 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 19:57:43 +0100 Subject: [PATCH 065/281] Fix tests after small updates --- tests/rpc/test_rpc_telegram.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ace44a34a..8264ab3df 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -58,7 +58,6 @@ def test__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) - assert telegram._updater is None assert telegram._config == default_conf @@ -881,7 +880,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: context.args = [] telegram._forcesell(update=update, context=context) assert msg_mock.call_count == 1 - assert 'invalid argument' in msg_mock.call_args_list[0][0][0] + assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0] # Invalid argument msg_mock.reset_mock() @@ -1251,7 +1250,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): context.args = [] telegram._delete_trade(update=update, context=context) - assert "invalid argument" in msg_mock.call_args_list[0][0][0] + assert "Trade-id not set." in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() create_mock_trades(fee) From d6cc3d737453fb6e6f0aad81d07bd63ddca2cbb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 19:58:06 +0100 Subject: [PATCH 066/281] Improve FAQ related to question in #4023 --- docs/faq.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index aa33218fb..3cf5a74ca 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## Beginner Tips & Tricks -* When you work with your strategy & hyperopt file you should use a proper code editor like vscode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). +* When you work with your strategy & hyperopt file you should use a proper code editor like VScode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). ## Freqtrade common issues @@ -17,7 +17,7 @@ This could have the following reasons: * The installation did not work correctly. * Please check the [Installation documentation](installation.md). -### I have waited 5 minutes, why hasn't the bot made any trades yet?! +### I have waited 5 minutes, why hasn't the bot made any trades yet? * Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry @@ -47,9 +47,9 @@ like pauses. You can stop your bot, adjust settings and start it again. That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -### Is there a setting to only SELL the coins being held and not perform anymore BUYS? +### Is there a setting to only Buys the coins being held and not perform anymore Buys? -You can use the `/forcesell all` command from Telegram. +You can use the `/stopbuy` to prevent future buys, followed `/forcesell all` (sell all open trades) command from Telegram. ### I want to run multiple bots on the same machine @@ -59,7 +59,7 @@ Please look at the [advanced setup documentation Page](advanced-setup.md#running This message is just a warning that the latest candles had missing candles in them. Depending on the exchange, this can indicate that the pair didn't have a trade for the timeframe you are using - and the exchange does only return candles with volume. -On low volume pairs, this is a rather common occurance. +On low volume pairs, this is a rather common occurrence. If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. @@ -130,7 +130,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us ### How many epoch do I need to get a good Hyperopt result? Per default Hyperopt called without the `-e`/`--epochs` command line option will only -run 100 epochs, means 100 evals of your triggers, guards, ... Too few +run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few to find a great result (unless if you are very lucky), so you probably have to run it for 10.000 or more. But it will take an eternity to compute. @@ -140,7 +140,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash -freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 +freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? @@ -151,21 +151,21 @@ freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDa This answer was written during the release 0.15.1, when we had: -- 8 triggers -- 9 guards: let's say we evaluate even 10 values from each -- 1 stoploss calculation: let's say we want 10 values from that too to be evaluated +* 8 triggers +* 9 guards: let's say we evaluate even 10 values from each +* 1 stoploss calculation: let's say we want 10 values from that too to be evaluated The following calculation is still very rough and not very precise but it will give the idea. With only these triggers and guards there is -already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. -Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th +already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations. +Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. * The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. -Example: +Example: `freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601` ## Edge module From c1fffb9925ea7d5f5fb661ac4a63f6eaff4b7754 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 1 Dec 2020 21:38:54 +0100 Subject: [PATCH 067/281] Update faq.md --- docs/faq.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 3cf5a74ca..e3a7895e3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,15 +2,15 @@ ## Beginner Tips & Tricks -* When you work with your strategy & hyperopt file you should use a proper code editor like VScode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). +* When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). ## Freqtrade common issues ### The bot does not start -Running the bot with `freqtrade trade --config config.json` does show the output `freqtrade: command not found`. +Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`. -This could have the following reasons: +This could be caused by the following reasons: * The virtual environment is not active * run `source .env/bin/activate` to activate the virtual environment @@ -20,12 +20,12 @@ This could have the following reasons: ### I have waited 5 minutes, why hasn't the bot made any trades yet? * Depending on the buy strategy, the amount of whitelisted coins, the -situation of the market etc, it can take up to hours to find good entry +situation of the market etc, it can take up to hours to find a good entry position for a trade. Be patient! -* Or it may because of a configuration error? Best check the logs, it's usually telling you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log). +* It may be because of a configuration error. It's best check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log). -### I have made 12 trades already, why is my total profit negative?! +### I have made 12 trades already, why is my total profit negative? I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our @@ -36,20 +36,18 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? +### I’d like to make changes to the config. Can I do that without having to kill the bot? -Not quite. Trades are persisted to a database but the configuration is -currently only read when the bot is killed and restarted. `/stop` more -like pauses. You can stop your bot, adjust settings and start it again. +Yes. You can edit your config, use the `/stop` command in Telegram, followed by `/reload_config` and the bot will run with the new config. ### I want to improve the bot with a new strategy That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -### Is there a setting to only Buys the coins being held and not perform anymore Buys? +### Is there a setting to only SELL the coins being held and not perform anymore BUYS? -You can use the `/stopbuy` to prevent future buys, followed `/forcesell all` (sell all open trades) command from Telegram. +You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forcesell all` (sell all open trades). ### I want to run multiple bots on the same machine @@ -73,7 +71,7 @@ Read [the Bittrex section about restricted markets](exchanges.md#restricted-mark ### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy -As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Probably your strategy was written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). +As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market": @@ -85,7 +83,7 @@ To fix it for Bittrex, redefine order types in the strategy to use "limit" inste } ``` -Same fix should be done in the configuration file, if order types are defined in your custom config rather than in the strategy. +The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. ### How do I search the bot logs for something? @@ -127,7 +125,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us ## Hyperopt module -### How many epoch do I need to get a good Hyperopt result? +### How many epochs do I need to get a good Hyperopt result? Per default Hyperopt called without the `-e`/`--epochs` command line option will only run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few From 4bc24ece41c61100fbd352166f588f401345cf55 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 1 Dec 2020 21:49:50 +0100 Subject: [PATCH 068/281] Update faq.md --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index e3a7895e3..1940b4e6c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -12,7 +12,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr This could be caused by the following reasons: -* The virtual environment is not active +* The virtual environment is not active. * run `source .env/bin/activate` to activate the virtual environment * The installation did not work correctly. * Please check the [Installation documentation](installation.md). From 3c4fe66d86e2eeb88f0a06791890478c1e3e4405 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 1 Dec 2020 21:50:51 +0100 Subject: [PATCH 069/281] Update faq.md --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 1940b4e6c..337b87ec8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -13,7 +13,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr This could be caused by the following reasons: * The virtual environment is not active. - * run `source .env/bin/activate` to activate the virtual environment + * Run `source .env/bin/activate` to activate the virtual environment. * The installation did not work correctly. * Please check the [Installation documentation](installation.md). From d039ce1fb3c84bc5535877f1afb04840a9d7c6cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Dec 2020 06:46:18 +0100 Subject: [PATCH 070/281] Update available columns for hyperopt closes #4025 --- docs/advanced-hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 59ebc16b5..1ace61769 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -77,7 +77,7 @@ Currently, the arguments are: * `results`: DataFrame containing the result The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): - `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` + `pair, profit_percent, profit_abs, open_date, open_rate, open_fee, close_date, close_rate, close_fee, amount, trade_duration, open_at_end, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame From c09c23eab15b920f757199fabebab9699d37ff4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Dec 2020 07:51:59 +0100 Subject: [PATCH 071/281] Make sure non-int telegram values don't crash the bot --- tests/rpc/test_rpc_telegram.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8264ab3df..33010484d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1222,8 +1222,14 @@ def test_telegram_trades(mocker, update, default_conf, fee): telegram._trades(update=update, context=context) assert "0 recent trades:" in msg_mock.call_args_list[0][0][0] assert "
" not in msg_mock.call_args_list[0][0][0]
-
     msg_mock.reset_mock()
+
+    context.args = ['hello']
+    telegram._trades(update=update, context=context)
+    assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "
" not in msg_mock.call_args_list[0][0][0]
+    msg_mock.reset_mock()
+
     create_mock_trades(fee)
 
     context = MagicMock()

From 9b4a81c0a4111e5779e07487ed570d717719d505 Mon Sep 17 00:00:00 2001
From: Samaoo 
Date: Wed, 2 Dec 2020 08:40:49 +0100
Subject: [PATCH 072/281] Update faq.md

---
 docs/faq.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/faq.md b/docs/faq.md
index 337b87ec8..b424cd31d 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -23,7 +23,7 @@ This could be caused by the following reasons:
 situation of the market etc, it can take up to hours to find a good entry
 position for a trade. Be patient!
 
-* It may be because of a configuration error. It's best check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log).
+* It may be because of a configuration error. It's best to check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log).
 
 ### I have made 12 trades already, why is my total profit negative?
 

From 2fbbeb970bb768de7c7efb3ed6b1ca2b0c922363 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 4 Dec 2020 07:42:16 +0100
Subject: [PATCH 073/281] Gracefully handle cases where no buy price was found

closes #4030
---
 freqtrade/freqtradebot.py  | 3 +++
 tests/test_freqtradebot.py | 6 ++++++
 2 files changed, 9 insertions(+)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 7416d8236..c8d281852 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -616,6 +616,9 @@ class FreqtradeBot:
             # Calculate price
             buy_limit_requested = self.get_buy_rate(pair, True)
 
+        if not buy_limit_requested:
+            raise PricingError('Could not determine buy price.')
+
         min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested)
         if min_stake_amount is not None and min_stake_amount > stake_amount:
             logger.warning(
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 64dfb016e..6adef510f 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1074,6 +1074,12 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
     mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
     assert not freqtrade.execute_buy(pair, stake_amount)
 
+    # Fail to get price...
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_buy_rate', MagicMock(return_value=0.0))
+
+    with pytest.raises(PricingError, match="Could not determine buy price."):
+        freqtrade.execute_buy(pair, stake_amount)
+
 
 def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)

From 7f453033a4928fd50eefc74bb737e03631f84bc8 Mon Sep 17 00:00:00 2001
From: Samaoo 
Date: Fri, 4 Dec 2020 16:53:41 +0100
Subject: [PATCH 074/281] Update edge.md

---
 docs/edge.md | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/docs/edge.md b/docs/edge.md
index 7442f1927..fd6d2cf7d 100644
--- a/docs/edge.md
+++ b/docs/edge.md
@@ -23,8 +23,8 @@ The Edge Positioning module seeks to improve a strategy's winning probability an
 We raise the following question[^1]:
 
 !!! Question "Which trade is a better option?"
-    a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
- b) A trade with 100% of chance of losing $30 + a) A trade with 80% of chance of losing 100\$ and 20% chance of winning 200\$
+ b) A trade with 100% of chance of losing 30\$ ???+ Info "Answer" The expected value of *a)* is smaller than the expected value of *b)*.
@@ -34,8 +34,8 @@ We raise the following question[^1]: Another way to look at it is to ask a similar question: !!! Question "Which trade is a better option?" - a) A trade with 80% of chance of winning 100 and 20% chance of losing $200
- b) A trade with 100% of chance of winning $30 + a) A trade with 80% of chance of winning 100\$ and 20% chance of losing 200\$
+ b) A trade with 100% of chance of winning 30\$ Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy. @@ -82,7 +82,7 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ???+ Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10). + Let's say that you think that the price of *stonecoin* today is 10.0\$. You believe that, because they will start mining stonecoin, it will go up to 15.0\$ tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to 0\$ tomorrow. You are planning to invest 100\$, which will give you 10 shares (100 / 10). Your potential profit is calculated as: @@ -92,9 +92,9 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= 50 \end{aligned}$ - Since the price might go to $0, the $100 dollars invested could turn into 0. + Since the price might go to 0\$, the 100\$ dollars invested could turn into 0. - We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$). + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$\). $\begin{aligned} \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\ @@ -109,7 +109,7 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= \frac{50}{15}\\ &= 3.33 \end{aligned}$
- What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested. + What it effectively means is that the strategy have the potential to make 3.33\$ for each 1\$ invested. On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: @@ -141,7 +141,7 @@ $$E = R * W - L$$ $E = R * W - L = 5 * 0.28 - 0.72 = 0.68$
-The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. +The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes 1.68\$ for every 1\$ it loses, on average. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. @@ -222,7 +222,7 @@ Edge module has following configuration options: | `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
**Datatype:** Float | `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges.
**Note** than having a smaller step means having a bigger range which could lead to slow calculation.
If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10.
*Defaults to `-0.001`.*
**Datatype:** Float | `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate.
This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
*Defaults to `0.60`.*
**Datatype:** Float -| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
*Defaults to `0.20`.*
**Datatype:** Float +| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10\$ on a trade you expect a 12\$ return.
*Defaults to `0.20`.*
**Datatype:** Float | `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
*Defaults to `10` (it is highly recommended not to decrease this number).*
**Datatype:** Integer | `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
**Datatype:** Integer | `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
*Defaults to `false`.*
**Datatype:** Boolean From 71e46794b44ca1315c9f78ef98c33aa52cc76fed Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Dec 2020 19:59:26 +0100 Subject: [PATCH 075/281] Add updating documentation closes #4036 --- docs/updating.md | 31 +++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 32 insertions(+) create mode 100644 docs/updating.md diff --git a/docs/updating.md b/docs/updating.md new file mode 100644 index 000000000..b23ce32dc --- /dev/null +++ b/docs/updating.md @@ -0,0 +1,31 @@ +# How to update + +To update your freqtrade installation, please use one of the below methods, corresponding to your installation method. + +## docker-compose + +!!! Note "Legacy installations using the `master` image" + We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` + +``` bash +docker-compose pull +docker-compose up -d +``` + +## Installation via setup script + +``` bash +./setup.sh --update +``` + +!!! Note + Make sure to run this command with your virtual environment disabled! + +## Plain native installation + +Please ensure that you're also updating dependencies - otherwise things might break without you noticing. + +``` bash +git pull +pip install -U -r requirements.txt +``` diff --git a/mkdocs.yml b/mkdocs.yml index 2cc0c9fcb..c791386ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,6 +31,7 @@ nav: - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md - Sandbox Testing: sandbox-testing.md + - Updating Freqtrade: updating.md - Deprecated Features: deprecated.md - Contributors Guide: developer.md theme: From 058d40a72c389ab90c643fe1cede812c4c5038b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 08:16:40 +0100 Subject: [PATCH 076/281] Fix telegram /daily command without arguments --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7239eab02..91306bf85 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -305,7 +305,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: - timescale = int(context.args[0]) if context.args else 0 + timescale = int(context.args[0]) if context.args else 7 except (TypeError, ValueError, IndexError): timescale = 7 try: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 33010484d..72d263bff 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -337,6 +337,18 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._daily(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + # Reset msg_mock msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 From c556d1b37e6e6367e1c05362522e494df274275b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:06:46 +0100 Subject: [PATCH 077/281] Make /stats working --- freqtrade/rpc/rpc.py | 4 ++++ freqtrade/rpc/telegram.py | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9ac271ba0..e17ee6b4f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -275,6 +275,10 @@ class RPC: "trades_count": len(output) } + def _rpc_stats(self): + trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) + return trades + def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: """ Returns cumulative profit statistics """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 074a6367f..29d2c6a01 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -782,22 +782,22 @@ class Telegram(RPC): """ # TODO: self._send_msg(...) def trade_win_loss(trade): - if trade['profit_abs'] > 0: + if trade.close_profit_abs > 0: return 'Wins' - elif trade['profit_abs'] < 0: + elif trade.close_profit_abs < 0: return 'Losses' else: return 'Draws' - trades = self._rpc_trade_history(-1) - trades_closed = [trade for trade in trades if not trade['is_open']] + trades = self._rpc_stats() + trades_closed = [trade for trade in trades if not trade.is_open] # Sell reason sell_reasons = {} for trade in trades_closed: - if trade['sell_reason'] not in sell_reasons: - sell_reasons[trade['sell_reason']] = {'Wins': 0, 'Losses': 0, 'Draws': 0} - sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 + if trade.sell_reason not in sell_reasons: + sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ @@ -814,8 +814,8 @@ class Telegram(RPC): # Duration dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} for trade in trades_closed: - if trade['close_date'] is not None and trade['open_date'] is not None: - trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) + if trade.close_date is not None and trade.open_date is not None: + trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' @@ -824,8 +824,9 @@ class Telegram(RPC): [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], headers=['', 'Duration'] ) + msg = (f"""```{sell_reasons_msg}```\n```{duration_msg}```""") - self._send_msg('\n'.join([sell_reasons_msg, duration_msg])) + self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: From 143423145cacec63868390045c1e911390dba327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:38:42 +0100 Subject: [PATCH 078/281] Refactor most of the logic to rpc.py this way /stats can be used by other RPC methods too --- freqtrade/rpc/rpc.py | 31 +++++++++++++++++++- freqtrade/rpc/telegram.py | 59 ++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e17ee6b4f..d7a59390d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -276,8 +276,37 @@ class RPC: } def _rpc_stats(self): + """ + Generate generic stats for trades in database + """ + def trade_win_loss(trade): + if trade.close_profit_abs > 0: + return 'Wins' + elif trade.close_profit_abs < 0: + return 'Losses' + else: + return 'Draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) - return trades + # Sell reason + sell_reasons = {} + for trade in trades: + if trade.sell_reason not in sell_reasons: + sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 + + # Duration + dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} + for trade in trades: + if trade.close_date is not None and trade.open_date is not None: + trade_dur = (trade.close_date - trade.open_date).total_seconds() + dur[trade_win_loss(trade)].append(trade_dur) + + wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' + draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' + losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + + durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} + return sell_reasons, durations def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 29d2c6a01..7c7007f86 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +from datetime import timedelta import json import logging from typing import Any, Callable, Dict, List, Union @@ -775,56 +776,44 @@ class Telegram(RPC): def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats - https://github.com/freqtrade/freqtrade/issues/3783 Show stats of recent trades - :param update: message update :return: None """ - # TODO: self._send_msg(...) - def trade_win_loss(trade): - if trade.close_profit_abs > 0: - return 'Wins' - elif trade.close_profit_abs < 0: - return 'Losses' - else: - return 'Draws' + sell_reasons, durations = self._rpc_stats() - trades = self._rpc_stats() - trades_closed = [trade for trade in trades if not trade.is_open] - - # Sell reason - sell_reasons = {} - for trade in trades_closed: - if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} - sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] + reason_map = { + 'roi': 'ROI', + 'stop_loss': 'Stoploss', + 'trailing_stop_loss': 'Trail. Stop', + 'stoploss_on_exchange': 'Stoploss', + 'sell_signal': 'Sell Signal', + 'force_sell': 'Forcesell', + 'emergency_sell': 'Emergency Sell', + } for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ - reason, sum(count.values()), + reason_map.get(reason, reason), + sum(count.values()), count['Wins'], - count['Draws'], + # count['Draws'], count['Losses'] ]) sell_reasons_msg = tabulate( sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) - # Duration - dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} - for trade in trades_closed: - if trade.close_date is not None and trade.open_date is not None: - trade_dur = (trade.close_date - trade.open_date).total_seconds() - dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' - draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' - losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' - duration_msg = tabulate( - [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], - headers=['', 'Duration'] + duration_msg = tabulate([ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + # ['Draws', str(timedelta(seconds=durations['draws']))], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] + ], + headers=['', 'Avg. Duration'] ) - msg = (f"""```{sell_reasons_msg}```\n```{duration_msg}```""") + msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") self._send_msg(msg, ParseMode.MARKDOWN) From aa27c9ace2fa3ac9b83780de5f1d0e4a9bd70fbf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:39:50 +0100 Subject: [PATCH 079/281] Reorder methods in telegram /stats is closely related to /profit --- freqtrade/rpc/telegram.py | 90 +++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7c7007f86..76d9292b4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -390,6 +390,51 @@ class Telegram(RPC): f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") self._send_msg(markdown_msg) + @authorized_only + def _stats(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /stats + Show stats of recent trades + :return: None + """ + sell_reasons, durations = self._rpc_stats() + + sell_reasons_tabulate = [] + reason_map = { + 'roi': 'ROI', + 'stop_loss': 'Stoploss', + 'trailing_stop_loss': 'Trail. Stop', + 'stoploss_on_exchange': 'Stoploss', + 'sell_signal': 'Sell Signal', + 'force_sell': 'Forcesell', + 'emergency_sell': 'Emergency Sell', + } + for reason, count in sell_reasons.items(): + sell_reasons_tabulate.append([ + reason_map.get(reason, reason), + sum(count.values()), + count['Wins'], + # count['Draws'], + count['Losses'] + ]) + sell_reasons_msg = tabulate( + sell_reasons_tabulate, + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] + ) + + duration_msg = tabulate([ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + # ['Draws', str(timedelta(seconds=durations['draws']))], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] + ], + headers=['', 'Avg. Duration'] + ) + msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") + + self._send_msg(msg, ParseMode.MARKDOWN) + @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ @@ -772,51 +817,6 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) - @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /stats - Show stats of recent trades - :return: None - """ - sell_reasons, durations = self._rpc_stats() - - sell_reasons_tabulate = [] - reason_map = { - 'roi': 'ROI', - 'stop_loss': 'Stoploss', - 'trailing_stop_loss': 'Trail. Stop', - 'stoploss_on_exchange': 'Stoploss', - 'sell_signal': 'Sell Signal', - 'force_sell': 'Forcesell', - 'emergency_sell': 'Emergency Sell', - } - for reason, count in sell_reasons.items(): - sell_reasons_tabulate.append([ - reason_map.get(reason, reason), - sum(count.values()), - count['Wins'], - # count['Draws'], - count['Losses'] - ]) - sell_reasons_msg = tabulate( - sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] - ) - - duration_msg = tabulate([ - ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], - # ['Draws', str(timedelta(seconds=durations['draws']))], - ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] - ], - headers=['', 'Avg. Duration'] - ) - msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") - - self._send_msg(msg, ParseMode.MARKDOWN) - @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 245c19f5e9aff5a797e0f1d71924d552b1f86a1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:48:56 +0100 Subject: [PATCH 080/281] Add simple test for /stats call --- freqtrade/rpc/rpc.py | 4 ++-- tests/conftest_trades.py | 2 ++ tests/rpc/test_rpc_telegram.py | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d7a59390d..c4b4117ff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -280,9 +280,9 @@ class RPC: Generate generic stats for trades in database """ def trade_win_loss(trade): - if trade.close_profit_abs > 0: + if trade.close_profit > 0: return 'Wins' - elif trade.close_profit_abs < 0: + elif trade.close_profit < 0: return 'Losses' else: return 'Draws' diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 78388f022..fac822b2b 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -82,6 +82,7 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', + sell_reason='sell_signal' ) o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) @@ -134,6 +135,7 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, + sell_reason='roi' ) o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 73a549860..725c1411e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -469,6 +469,41 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] +def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, + limit_buy_order, limit_sell_order, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) + + telegram._stats(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + # assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + + # Create some test data + create_mock_trades(fee) + + telegram._stats(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'Sell Reason' in msg_mock.call_args_list[-1][0][0] + assert 'ROI' in msg_mock.call_args_list[-1][0][0] + assert 'Avg. Duration' in msg_mock.call_args_list[-1][0][0] + msg_mock.reset_mock() + + def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) From 51fbd0698c63f0ab909e45532d4dc3e64cee35a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Dec 2020 19:57:48 +0100 Subject: [PATCH 081/281] Move get_logs to be static method --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 3 ++- freqtrade/rpc/telegram.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8c2c203e6..e8eaef933 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -388,7 +388,7 @@ class ApiServer(RPC): limit: Only get a certain number of records """ limit = int(request.args.get('limit', 0)) or None - return jsonify(self._rpc_get_logs(limit)) + return jsonify(RPC._rpc_get_logs(limit)) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9ac271ba0..42f26fe74 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -645,7 +645,8 @@ class RPC: } return res - def _rpc_get_logs(self, limit: Optional[int]) -> Dict[str, Any]: + @staticmethod + def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]: """Returns the last X logs""" if limit: buffer = bufferHandler.buffer[-limit:] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 91306bf85..26e87e654 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -683,7 +683,7 @@ class Telegram(RPC): limit = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): limit = 10 - logs = self._rpc_get_logs(limit)['logs'] + logs = RPC._rpc_get_logs(limit)['logs'] msgs = '' msg_template = "*{}* {}: {} \\- `{}`" for logrec in logs: From 0c0eb8236d24f84efaad4b51bd768ff23ea462f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 05:48:23 +0000 Subject: [PATCH 082/281] Bump mkdocs-material from 6.1.6 to 6.1.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.6 to 6.1.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.6...6.1.7) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 87bc6dfdd..2b133cb07 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.6 +mkdocs-material==6.1.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 647e6509a477b9b07b7e9f9d73ee3ad756d2b499 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 05:48:43 +0000 Subject: [PATCH 083/281] Bump ccxt from 1.38.55 to 1.38.87 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.38.55 to 1.38.87. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.38.55...1.38.87) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f59754f93..105839f0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.38.55 +ccxt==1.38.87 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From b6b9c8e5cc401bb5f876d74515b652cb5a5e6537 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 07:11:08 +0200 Subject: [PATCH 084/281] Move "slow-log" to it's own mixin --- freqtrade/mixins/__init__.py | 2 ++ freqtrade/mixins/logging_mixin.py | 34 +++++++++++++++++++++++++++++++ freqtrade/pairlist/IPairList.py | 25 +++-------------------- 3 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 freqtrade/mixins/__init__.py create mode 100644 freqtrade/mixins/logging_mixin.py diff --git a/freqtrade/mixins/__init__.py b/freqtrade/mixins/__init__.py new file mode 100644 index 000000000..f4a640fa3 --- /dev/null +++ b/freqtrade/mixins/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.mixins.logging_mixin import LoggingMixin diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py new file mode 100644 index 000000000..4e19e45a4 --- /dev/null +++ b/freqtrade/mixins/logging_mixin.py @@ -0,0 +1,34 @@ + + +from cachetools import TTLCache, cached + + +class LoggingMixin(): + """ + Logging Mixin + Shows similar messages only once every `refresh_period`. + """ + def __init__(self, logger, refresh_period: int = 3600): + """ + :param refresh_period: in seconds - Show identical messages in this intervals + """ + self.logger = logger + self.refresh_period = refresh_period + self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) + + def log_on_refresh(self, logmethod, message: str) -> None: + """ + Logs message - not more often than "refresh_period" to avoid log spamming + Logs the log-message as debug as well to simplify debugging. + :param logmethod: Function that'll be called. Most likely `logger.info`. + :param message: String containing the message to be sent to the function. + :return: None. + """ + @cached(cache=self._log_cache) + def _log_on_refresh(message: str): + logmethod(message) + + # Log as debug first + self.logger.debug(message) + # Call hidden function. + _log_on_refresh(message) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index c869e499b..5f29241ce 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -6,16 +6,15 @@ from abc import ABC, abstractmethod, abstractproperty from copy import deepcopy from typing import Any, Dict, List -from cachetools import TTLCache, cached - from freqtrade.exceptions import OperationalException from freqtrade.exchange import market_is_active +from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) -class IPairList(ABC): +class IPairList(LoggingMixin, ABC): def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], @@ -36,7 +35,7 @@ class IPairList(ABC): self._pairlist_pos = pairlist_pos self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) self._last_refresh = 0 - self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) + LoggingMixin.__init__(self, logger, self.refresh_period) @property def name(self) -> str: @@ -46,24 +45,6 @@ class IPairList(ABC): """ return self.__class__.__name__ - def log_on_refresh(self, logmethod, message: str) -> None: - """ - Logs message - not more often than "refresh_period" to avoid log spamming - Logs the log-message as debug as well to simplify debugging. - :param logmethod: Function that'll be called. Most likely `logger.info`. - :param message: String containing the message to be sent to the function. - :return: None. - """ - - @cached(cache=self._log_cache) - def _log_on_refresh(message: str): - logmethod(message) - - # Log as debug first - logger.debug(message) - # Call hidden function. - _log_on_refresh(message) - @abstractproperty def needstickers(self) -> bool: """ From a0bd2ce837bb61a4e335a1980239f536101e3a70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 08:06:29 +0200 Subject: [PATCH 085/281] Add first version of protection manager --- freqtrade/plugins/__init__.py | 0 freqtrade/plugins/protectionmanager.py | 51 ++++++++++++++++++++ freqtrade/plugins/protections/__init__.py | 2 + freqtrade/plugins/protections/iprotection.py | 24 +++++++++ freqtrade/resolvers/__init__.py | 1 + freqtrade/resolvers/protection_resolver.py | 44 +++++++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 freqtrade/plugins/__init__.py create mode 100644 freqtrade/plugins/protectionmanager.py create mode 100644 freqtrade/plugins/protections/__init__.py create mode 100644 freqtrade/plugins/protections/iprotection.py create mode 100644 freqtrade/resolvers/protection_resolver.py diff --git a/freqtrade/plugins/__init__.py b/freqtrade/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py new file mode 100644 index 000000000..ff64ca789 --- /dev/null +++ b/freqtrade/plugins/protectionmanager.py @@ -0,0 +1,51 @@ +""" +Protection manager class +""" +import logging +from typing import Dict, List + +from freqtrade.exceptions import OperationalException +from freqtrade.plugins.protections import IProtection +from freqtrade.resolvers import ProtectionResolver + + +logger = logging.getLogger(__name__) + + +class ProtectionManager(): + + def __init__(self, exchange, config: dict) -> None: + self._exchange = exchange + self._config = config + + self._protection_handlers: List[IProtection] = [] + self._tickers_needed = False + for protection_handler_config in self._config.get('protections', None): + if 'method' not in protection_handler_config: + logger.warning(f"No method found in {protection_handler_config}, ignoring.") + continue + protection_handler = ProtectionResolver.load_protection( + protection_handler_config['method'], + exchange=exchange, + protectionmanager=self, + config=config, + protection_config=protection_handler_config, + ) + self._tickers_needed |= protection_handler.needstickers + self._protection_handlers.append(protection_handler) + + if not self._protection_handlers: + raise OperationalException("No protection Handlers defined") + + @property + def name_list(self) -> List[str]: + """ + Get list of loaded Protection Handler names + """ + return [p.name for p in self._protection_handlers] + + def short_desc(self) -> List[Dict]: + """ + List of short_desc for each Pairlist Handler + """ + return [{p.name: p.short_desc()} for p in self._pairlist_handlers] diff --git a/freqtrade/plugins/protections/__init__.py b/freqtrade/plugins/protections/__init__.py new file mode 100644 index 000000000..5ecae7888 --- /dev/null +++ b/freqtrade/plugins/protections/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.plugins.protections.iprotection import IProtection diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py new file mode 100644 index 000000000..b10856f70 --- /dev/null +++ b/freqtrade/plugins/protections/iprotection.py @@ -0,0 +1,24 @@ + +import logging +from abc import ABC, abstractmethod +from typing import Any, Dict + + +logger = logging.getLogger(__name__) + + +class IProtection(ABC): + + def __init__(self, config: Dict[str, Any]) -> None: + self._config = config + + @property + def name(self) -> str: + return self.__class__.__name__ + + @abstractmethod + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + -> Please overwrite in subclasses + """ diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index b42ec4931..ef24bf481 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -6,6 +6,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver # Don't import HyperoptResolver to avoid loading the whole Optimize tree # from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.resolvers.pairlist_resolver import PairListResolver +from freqtrade.resolvers.protection_resolver import ProtectionResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py new file mode 100644 index 000000000..9a85104c3 --- /dev/null +++ b/freqtrade/resolvers/protection_resolver.py @@ -0,0 +1,44 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom pairlists +""" +import logging +from pathlib import Path +from typing import Dict + +from freqtrade.plugins.protections import IProtection +from freqtrade.resolvers import IResolver + + +logger = logging.getLogger(__name__) + + +class ProtectionResolver(IResolver): + """ + This class contains all the logic to load custom PairList class + """ + object_type = IProtection + object_type_str = "Protection" + user_subdir = None + initial_search_path = Path(__file__).parent.parent.joinpath('plugins/protections').resolve() + + @staticmethod + def load_protection(protection_name: str, exchange, protectionmanager, + config: Dict, protection_config: Dict) -> IProtection: + """ + Load the protection with protection_name + :param protection_name: Classname of the pairlist + :param exchange: Initialized exchange class + :param protectionmanager: Initialized protection manager + :param config: configuration dictionary + :param protection_config: Configuration dedicated to this pairlist + :return: initialized Protection class + """ + return ProtectionResolver.load_object(protection_name, config, + kwargs={'exchange': exchange, + 'pairlistmanager': protectionmanager, + 'config': config, + 'pairlistconfig': protection_config, + }, + ) From 3447f1ae531733eabd620b15b849fb48e204ae6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 07:40:44 +0200 Subject: [PATCH 086/281] Implement first stop method --- config_full.json.example | 7 +++ docs/includes/protections.md | 36 ++++++++++++ freqtrade/plugins/protectionmanager.py | 5 +- freqtrade/plugins/protections/iprotection.py | 11 +++- .../plugins/protections/stoploss_guard.py | 55 +++++++++++++++++++ freqtrade/resolvers/protection_resolver.py | 11 +--- 6 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 docs/includes/protections.md create mode 100644 freqtrade/plugins/protections/stoploss_guard.py diff --git a/config_full.json.example b/config_full.json.example index 5ee2a1faf..96aa82d5f 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -75,6 +75,13 @@ "refresh_period": 1440 } ], + "protections": [ + { + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 4 + } + ], "exchange": { "name": "bittrex", "sandbox": false, diff --git a/docs/includes/protections.md b/docs/includes/protections.md new file mode 100644 index 000000000..078ba0c2b --- /dev/null +++ b/docs/includes/protections.md @@ -0,0 +1,36 @@ +## Protections + +Protections will protect your strategy from unexpected events and market conditions. + +### Available Protection Handlers + +* [`StoplossGuard`](#stoploss-guard) (default, if not configured differently) + +#### Stoploss Guard + +`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case it will stop trading until this condition is no longer true. + +```json +"protections": [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 4 +}], +``` + +!!! Note + `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. + +### Full example of Protections + +The below example stops trading if more than 4 stoploss occur within a 1 hour (60 minute) limit. + +```json +"protections": [ + { + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 4 + } + ], +``` diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index ff64ca789..5185c93f0 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -14,8 +14,7 @@ logger = logging.getLogger(__name__) class ProtectionManager(): - def __init__(self, exchange, config: dict) -> None: - self._exchange = exchange + def __init__(self, config: dict) -> None: self._config = config self._protection_handlers: List[IProtection] = [] @@ -26,8 +25,6 @@ class ProtectionManager(): continue protection_handler = ProtectionResolver.load_protection( protection_handler_config['method'], - exchange=exchange, - protectionmanager=self, config=config, protection_config=protection_handler_config, ) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index b10856f70..75d1fb3ad 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,6 +1,7 @@ import logging from abc import ABC, abstractmethod +from datetime import datetime from typing import Any, Dict @@ -9,8 +10,9 @@ logger = logging.getLogger(__name__) class IProtection(ABC): - def __init__(self, config: Dict[str, Any]) -> None: + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config + self._protection_config = protection_config @property def name(self) -> str: @@ -22,3 +24,10 @@ class IProtection(ABC): Short method description - used for startup-messages -> Please overwrite in subclasses """ + + @abstractmethod + def stop_trade_enters_global(self, date_now: datetime) -> bool: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + """ diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py new file mode 100644 index 000000000..3418dd1da --- /dev/null +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -0,0 +1,55 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +from sqlalchemy import or_, and_ + +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection +from freqtrade.strategy.interface import SellType + + +logger = logging.getLogger(__name__) + + +class StoplossGuard(IProtection): + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + self._lookback_period = protection_config.get('lookback_period', 60) + self._trade_limit = protection_config.get('trade_limit', 10) + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return f"{self.name} - Frequent Stoploss Guard" + + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> bool: + """ + Evaluate recent trades + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + or_(Trade.sell_reason == SellType.STOP_LOSS.value, + and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, + Trade.close_profit < 0)) + ] + if pair: + filters.append(Trade.pair == pair) + trades = Trade.get_trades(filters).all() + + if len(trades) > self.trade_limit: + return True + + return False + + def stop_trade_enters_global(self, date_now: datetime) -> bool: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + """ + return self._stoploss_guard(date_now, pair=None) diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py index 9a85104c3..928bd4633 100644 --- a/freqtrade/resolvers/protection_resolver.py +++ b/freqtrade/resolvers/protection_resolver.py @@ -24,21 +24,16 @@ class ProtectionResolver(IResolver): initial_search_path = Path(__file__).parent.parent.joinpath('plugins/protections').resolve() @staticmethod - def load_protection(protection_name: str, exchange, protectionmanager, - config: Dict, protection_config: Dict) -> IProtection: + def load_protection(protection_name: str, config: Dict, protection_config: Dict) -> IProtection: """ Load the protection with protection_name :param protection_name: Classname of the pairlist - :param exchange: Initialized exchange class - :param protectionmanager: Initialized protection manager :param config: configuration dictionary :param protection_config: Configuration dedicated to this pairlist :return: initialized Protection class """ return ProtectionResolver.load_object(protection_name, config, - kwargs={'exchange': exchange, - 'pairlistmanager': protectionmanager, - 'config': config, - 'pairlistconfig': protection_config, + kwargs={'config': config, + 'protection_config': protection_config, }, ) From 04878c3ce1806536dcb46d9bbdc1dd38b32f88fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 07:41:41 +0200 Subject: [PATCH 087/281] Rename test directory for pairlist --- tests/{pairlist => plugins}/__init__.py | 0 tests/{pairlist => plugins}/test_pairlist.py | 0 tests/{pairlist => plugins}/test_pairlocks.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{pairlist => plugins}/__init__.py (100%) rename tests/{pairlist => plugins}/test_pairlist.py (100%) rename tests/{pairlist => plugins}/test_pairlocks.py (100%) diff --git a/tests/pairlist/__init__.py b/tests/plugins/__init__.py similarity index 100% rename from tests/pairlist/__init__.py rename to tests/plugins/__init__.py diff --git a/tests/pairlist/test_pairlist.py b/tests/plugins/test_pairlist.py similarity index 100% rename from tests/pairlist/test_pairlist.py rename to tests/plugins/test_pairlist.py diff --git a/tests/pairlist/test_pairlocks.py b/tests/plugins/test_pairlocks.py similarity index 100% rename from tests/pairlist/test_pairlocks.py rename to tests/plugins/test_pairlocks.py From 246b4a57a40f25750840e515d6f8119c2a5be291 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 19:24:09 +0200 Subject: [PATCH 088/281] add small note to pairlist dev docs --- docs/developer.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index c253f4460..662905d65 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -119,6 +119,9 @@ The base-class provides an instance of the exchange (`self._exchange`) the pairl self._pairlist_pos = pairlist_pos ``` +!!! Note + You'll need to register your pairlist in `constants.py` under the variable `AVAILABLE_PAIRLISTS` - otherwise it will not be selectable. + Now, let's step through the methods which require actions: #### Pairlist configuration From f39a534fc039795af1eb45761d998b221e9a1867 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 20:03:56 +0200 Subject: [PATCH 089/281] Implement global stop (First try) --- freqtrade/constants.py | 1 + freqtrade/freqtradebot.py | 5 ++++- freqtrade/plugins/__init__.py | 2 ++ freqtrade/plugins/protectionmanager.py | 20 +++++++++++++------ freqtrade/plugins/protections/iprotection.py | 2 +- .../plugins/protections/stoploss_guard.py | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 601e525c1..d070386d0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,6 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] +AVAILABLE_PROTECTIONS = ['StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8d281852..2dbd7f099 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -23,6 +23,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db +from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -78,6 +79,8 @@ class FreqtradeBot: self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) + self.protections = ProtectionManager(self.config) + # Attach Dataprovider to Strategy baseclass IStrategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass @@ -178,7 +181,7 @@ class FreqtradeBot: self.exit_positions(trades) # Then looking for buy opportunities - if self.get_free_open_trades(): + if self.get_free_open_trades() and not self.protections.global_stop(): self.enter_positions() Trade.session.flush() diff --git a/freqtrade/plugins/__init__.py b/freqtrade/plugins/__init__.py index e69de29bb..96943268b 100644 --- a/freqtrade/plugins/__init__.py +++ b/freqtrade/plugins/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +# from freqtrade.plugins.protectionmanager import ProtectionManager diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 5185c93f0..31b0ca300 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -7,7 +7,7 @@ from typing import Dict, List from freqtrade.exceptions import OperationalException from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver - +from datetime import datetime logger = logging.getLogger(__name__) @@ -18,8 +18,7 @@ class ProtectionManager(): self._config = config self._protection_handlers: List[IProtection] = [] - self._tickers_needed = False - for protection_handler_config in self._config.get('protections', None): + for protection_handler_config in self._config.get('protections', []): if 'method' not in protection_handler_config: logger.warning(f"No method found in {protection_handler_config}, ignoring.") continue @@ -28,11 +27,10 @@ class ProtectionManager(): config=config, protection_config=protection_handler_config, ) - self._tickers_needed |= protection_handler.needstickers self._protection_handlers.append(protection_handler) if not self._protection_handlers: - raise OperationalException("No protection Handlers defined") + logger.info("No protection Handlers defined.") @property def name_list(self) -> List[str]: @@ -45,4 +43,14 @@ class ProtectionManager(): """ List of short_desc for each Pairlist Handler """ - return [{p.name: p.short_desc()} for p in self._pairlist_handlers] + return [{p.name: p.short_desc()} for p in self._protection_handlers] + + def global_stop(self) -> bool: + now = datetime.utcnow() + for protection_handler in self._protection_handlers: + result = protection_handler.global_stop(now) + + # Early stopping - first positive result stops the application + if result: + return True + return False diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 75d1fb3ad..25bcee923 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -26,7 +26,7 @@ class IProtection(ABC): """ @abstractmethod - def stop_trade_enters_global(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> bool: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 3418dd1da..c6cddb01e 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -47,7 +47,7 @@ class StoplossGuard(IProtection): return False - def stop_trade_enters_global(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> bool: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". From 816703b8e112d161727367664b7bbfdf2159b5d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 07:38:00 +0200 Subject: [PATCH 090/281] Improve protections work --- freqtrade/constants.py | 11 ++++++++++- freqtrade/plugins/protectionmanager.py | 4 +++- freqtrade/plugins/protections/iprotection.py | 5 ++++- freqtrade/plugins/protections/stoploss_guard.py | 6 ++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d070386d0..9a93bfae3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -193,7 +193,16 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, - 'config': {'type': 'object'} + }, + 'required': ['method'], + } + }, + 'protections': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, }, 'required': ['method'], } diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 31b0ca300..dd6076ec1 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -2,12 +2,13 @@ Protection manager class """ import logging +from datetime import datetime from typing import Dict, List from freqtrade.exceptions import OperationalException from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver -from datetime import datetime + logger = logging.getLogger(__name__) @@ -47,6 +48,7 @@ class ProtectionManager(): def global_stop(self) -> bool: now = datetime.utcnow() + for protection_handler in self._protection_handlers: result = protection_handler.global_stop(now) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 25bcee923..ecb4cad09 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -4,15 +4,18 @@ from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Dict +from freqtrade.mixins import LoggingMixin + logger = logging.getLogger(__name__) -class IProtection(ABC): +class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config + LoggingMixin.__init__(self, logger) @property def name(self) -> str: diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index c6cddb01e..3b0b8c773 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -3,7 +3,7 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict -from sqlalchemy import or_, and_ +from sqlalchemy import and_, or_ from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection @@ -42,7 +42,9 @@ class StoplossGuard(IProtection): filters.append(Trade.pair == pair) trades = Trade.get_trades(filters).all() - if len(trades) > self.trade_limit: + if len(trades) > self._trade_limit: + self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.") return True return False From 2b85e7eac3b8936d777eb751f15899afd4aa214f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 07:59:32 +0200 Subject: [PATCH 091/281] Add initial tests for StoplossGuard protection --- tests/plugins/test_protections.py | 79 +++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/plugins/test_protections.py diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py new file mode 100644 index 000000000..2bb0886ee --- /dev/null +++ b/tests/plugins/test_protections.py @@ -0,0 +1,79 @@ +from freqtrade.strategy.interface import SellType +from unittest.mock import MagicMock, PropertyMock +import random +import pytest +from datetime import datetime, timedelta + +from freqtrade.constants import AVAILABLE_PROTECTIONS +from freqtrade.persistence import Trade +from tests.conftest import get_patched_freqtradebot, log_has_re + + +def generate_mock_trade(pair: str, fee: float, is_open: bool, + sell_reason: str = SellType.SELL_SIGNAL, + min_ago_open: int = None, min_ago_close: int = None, + ): + open_rate = random.random() + + trade = Trade( + pair=pair, + stake_amount=0.01, + fee_open=fee, + fee_close=fee, + open_date=datetime.utcnow() - timedelta(minutes=min_ago_open or 200), + close_date=datetime.utcnow() - timedelta(minutes=min_ago_close or 30), + open_rate=open_rate, + is_open=is_open, + amount=0.01 / open_rate, + exchange='bittrex', + ) + trade.recalc_open_trade_price() + if not is_open: + trade.close(open_rate * (1 - 0.9)) + trade.sell_reason = sell_reason + return trade + + +@pytest.mark.usefixtures("init_persistence") +def test_stoploss_guard(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 2 + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, + )) + + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + # This trade does not count, as it's closed too long ago + Trade.session.add(generate_mock_trade( + 'BCH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=250, min_ago_close=100, + )) + + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=240, min_ago_close=30, + )) + # 3 Trades closed - but the 2nd has been closed too long ago. + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'LTC/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=180, min_ago_close=30, + )) + + assert freqtrade.protections.global_stop() + assert log_has_re(message, caplog) From 56975db2ed5387e8a14b5a17e290c0d45a0cdba6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 08:07:09 +0200 Subject: [PATCH 092/281] Add more tests --- config_full.json.example | 3 ++- freqtrade/plugins/protectionmanager.py | 13 ++++----- freqtrade/plugins/protections/__init__.py | 2 +- freqtrade/plugins/protections/iprotection.py | 6 +++-- .../plugins/protections/stoploss_guard.py | 27 ++++++++++++++----- tests/plugins/test_protections.py | 24 ++++++++++++++--- 6 files changed, 54 insertions(+), 21 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 96aa82d5f..eb20065ce 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -79,7 +79,8 @@ { "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stopduration": 60 } ], "exchange": { diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index dd6076ec1..c4822a323 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -2,10 +2,10 @@ Protection manager class """ import logging -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, List -from freqtrade.exceptions import OperationalException +from freqtrade.persistence import PairLocks from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver @@ -47,12 +47,13 @@ class ProtectionManager(): return [{p.name: p.short_desc()} for p in self._protection_handlers] def global_stop(self) -> bool: - now = datetime.utcnow() + now = datetime.now(timezone.utc) for protection_handler in self._protection_handlers: - result = protection_handler.global_stop(now) + result, until, reason = protection_handler.global_stop(now) - # Early stopping - first positive result stops the application - if result: + # Early stopping - first positive result blocks further trades + if result and until: + PairLocks.lock_pair('*', until, reason) return True return False diff --git a/freqtrade/plugins/protections/__init__.py b/freqtrade/plugins/protections/__init__.py index 5ecae7888..936355052 100644 --- a/freqtrade/plugins/protections/__init__.py +++ b/freqtrade/plugins/protections/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa: F401 -from freqtrade.plugins.protections.iprotection import IProtection +from freqtrade.plugins.protections.iprotection import IProtection, ProtectionReturn diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index ecb4cad09..cadf01184 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -2,13 +2,15 @@ import logging from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Dict +from typing import Any, Dict, Optional, Tuple from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) +ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str]] + class IProtection(LoggingMixin, ABC): @@ -29,7 +31,7 @@ class IProtection(LoggingMixin, ABC): """ @abstractmethod - def global_stop(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> ProtectionReturn: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 3b0b8c773..db3655a38 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -1,12 +1,12 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict +from typing import Any, Dict, Tuple from sqlalchemy import and_, or_ from freqtrade.persistence import Trade -from freqtrade.plugins.protections import IProtection +from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.strategy.interface import SellType @@ -17,16 +17,26 @@ class StoplossGuard(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) + self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) + self._stopduration = protection_config.get('stopduration', 60) + + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' + f'locking for {self._stopduration} min.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return f"{self.name} - Frequent Stoploss Guard" + return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " + f"within {self._lookback_period} minutes.") - def _stoploss_guard(self, date_now: datetime, pair: str = None) -> bool: + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> ProtectionReturn: """ Evaluate recent trades """ @@ -45,13 +55,16 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - return True + until = date_now + timedelta(minutes=self._stopduration) + return True, until, self._reason() - return False + return False, None, None - def global_stop(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> ProtectionReturn: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until """ return self._stoploss_guard(date_now, pair=None) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2bb0886ee..d2815338e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,11 +1,10 @@ -from freqtrade.strategy.interface import SellType -from unittest.mock import MagicMock, PropertyMock import random -import pytest from datetime import datetime, timedelta -from freqtrade.constants import AVAILABLE_PROTECTIONS +import pytest + from freqtrade.persistence import Trade +from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -77,3 +76,20 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert freqtrade.protections.global_stop() assert log_has_re(message, caplog) + + +@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ + ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses within 60 minutes.'}]", + None + ), +]) +def test_protection_manager_desc(mocker, default_conf, protectionconf, + desc_expected, exception_expected): + + default_conf['protections'] = [protectionconf] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + short_desc = str(freqtrade.protections.short_desc()) + assert short_desc == desc_expected From 05be33ccd4240ccf39a92f122637003cea530854 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 13:55:54 +0200 Subject: [PATCH 093/281] Simplify is_pair_locked --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6027908da..04d5a7695 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -688,7 +688,7 @@ class PairLock(_DECL_BASE): @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ - Get all locks for this pair + Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). """ From ff7ba23477d819ec3e3e1b91edc87149ee68efbc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 14:46:13 +0200 Subject: [PATCH 094/281] Simplify enter_positions and add global pairlock check --- freqtrade/freqtradebot.py | 10 +++++++--- tests/test_freqtradebot.py | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2dbd7f099..75ff07b17 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -180,8 +180,10 @@ class FreqtradeBot: # First process current opened trades (positions) self.exit_positions(trades) + # Evaluate if protections should apply + self.protections.global_stop() # Then looking for buy opportunities - if self.get_free_open_trades() and not self.protections.global_stop(): + if self.get_free_open_trades(): self.enter_positions() Trade.session.flush() @@ -361,6 +363,9 @@ class FreqtradeBot: logger.info("No currency pair in active pair whitelist, " "but checking to sell open trades.") return trades_created + if PairLocks.is_global_lock(): + logger.info("Global pairlock active. Not creating new trades.") + return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: try: @@ -369,8 +374,7 @@ class FreqtradeBot: logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. " - "Trying again...") + logger.debug("Found no buy signals for whitelisted currencies. Trying again...") return trades_created diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6adef510f..94ed06cd9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State @@ -678,6 +678,32 @@ def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_o assert log_has("Active pair whitelist is empty.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, fee, + mocker, caplog) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + n = freqtrade.enter_positions() + message = "Global pairlock active. Not creating new trades." + n = freqtrade.enter_positions() + # 0 trades, but it's not because of pairlock. + assert n == 0 + assert not log_has(message, caplog) + + PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because') + n = freqtrade.enter_positions() + assert n == 0 + assert log_has(message, caplog) + + def test_create_trade_no_signal(default_conf, fee, mocker) -> None: default_conf['dry_run'] = True From 2a66c33a4e2c6ae4d116321a2fd8b46638f34354 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 16:52:26 +0200 Subject: [PATCH 095/281] Add locks per pair --- config_full.json.example | 4 ++ freqtrade/constants.py | 2 +- freqtrade/plugins/protectionmanager.py | 9 +++ .../plugins/protections/cooldown_period.py | 68 +++++++++++++++++++ freqtrade/plugins/protections/iprotection.py | 9 +++ .../plugins/protections/stoploss_guard.py | 11 ++- 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 freqtrade/plugins/protections/cooldown_period.py diff --git a/config_full.json.example b/config_full.json.example index eb20065ce..839f99dbd 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -81,6 +81,10 @@ "lookback_period": 60, "trade_limit": 4, "stopduration": 60 + }, + { + "method": "CooldownPeriod", + "stopduration": 20 } ], "exchange": { diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9a93bfae3..d06047f4c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard'] +AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index c4822a323..b0929af88 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -57,3 +57,12 @@ class ProtectionManager(): PairLocks.lock_pair('*', until, reason) return True return False + + def stop_per_pair(self, pair) -> bool: + now = datetime.now(timezone.utc) + for protection_handler in self._protection_handlers: + result, until, reason = protection_handler.stop_per_pair(pair, now) + if result and until: + PairLocks.lock_pair(pair, until, reason) + return True + return False diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py new file mode 100644 index 000000000..c6b6685b2 --- /dev/null +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -0,0 +1,68 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + + +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class CooldownPeriod(IProtection): + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._stopduration = protection_config.get('stopduration', 60) + + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'Cooldown period for {self._stopduration} min.') + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Cooldown period.") + + def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: + """ + Get last trade for this pair + """ + look_back_until = date_now - timedelta(minutes=self._stopduration) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + Trade.pair == pair, + ] + trade = Trade.get_trades(filters).first() + if trade: + self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stopduration}.") + until = trade.close_date + timedelta(minutes=self._stopduration) + return True, until, self._reason() + + return False, None, None + + def global_stop(self, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + # Not implemented for cooldown period. + return False, None, None + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return self._cooldown_period(pair, date_now) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index cadf01184..5dbcf72f6 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -36,3 +36,12 @@ class IProtection(LoggingMixin, ABC): Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". """ + + @abstractmethod + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index db3655a38..18888b854 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict, Tuple +from typing import Any, Dict from sqlalchemy import and_, or_ @@ -68,3 +68,12 @@ class StoplossGuard(IProtection): If true, all pairs will be locked with until """ return self._stoploss_guard(date_now, pair=None) + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return False, None, None From fe0afb98832e662fbec06decc951143e8e5c113b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 17:09:30 +0200 Subject: [PATCH 096/281] Implement calling of per-pair protection --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 75ff07b17..7bfd64c2d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1415,6 +1415,7 @@ class FreqtradeBot: # Updating wallets when order is closed if not trade.is_open: + self.protections.stop_per_pair(trade.pair) self.wallets.update() return False From 8dbef6bbeab0662a5014082f7ab65e2abb63d1ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 20:25:40 +0200 Subject: [PATCH 097/281] Add test for cooldown period --- .../plugins/protections/cooldown_period.py | 8 ++-- tests/plugins/test_protections.py | 43 ++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index c6b6685b2..24f55419b 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,9 +1,8 @@ import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict - from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -28,7 +27,7 @@ class CooldownPeriod(IProtection): """ Short method description - used for startup-messages """ - return (f"{self.name} - Cooldown period.") + return (f"{self.name} - Cooldown period of {self._stopduration} min.") def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: """ @@ -43,7 +42,8 @@ class CooldownPeriod(IProtection): trade = Trade.get_trades(filters).first() if trade: self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stopduration}.") - until = trade.close_date + timedelta(minutes=self._stopduration) + until = trade.close_date.replace( + tzinfo=timezone.utc) + timedelta(minutes=self._stopduration) return True, until, self._reason() return False, None, None diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index d2815338e..59ada7c1e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import pytest -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -76,6 +76,43 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert freqtrade.protections.global_stop() assert log_has_re(message, caplog) + assert PairLocks.is_global_lock() + + +@pytest.mark.usefixtures("init_persistence") +def test_CooldownPeriod(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "CooldownPeriod", + "stopduration": 60, + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, + )) + + assert not freqtrade.protections.global_stop() + assert freqtrade.protections.stop_per_pair('XRP/BTC') + assert PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=205, min_ago_close=35, + )) + + assert not freqtrade.protections.global_stop() + assert not PairLocks.is_pair_locked('ETH/BTC') + assert freqtrade.protections.stop_per_pair('ETH/BTC') + assert PairLocks.is_pair_locked('ETH/BTC') + assert not PairLocks.is_global_lock() @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ @@ -84,6 +121,10 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): "2 stoplosses within 60 minutes.'}]", None ), + ({"method": "CooldownPeriod", "stopduration": 60}, + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From 9f6c2a583fff165fd56d935cdd93f47aba13cbb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 07:48:27 +0100 Subject: [PATCH 098/281] Better wording for config options --- freqtrade/constants.py | 5 ++++- .../plugins/protections/cooldown_period.py | 12 ++++++------ .../plugins/protections/stoploss_guard.py | 18 +++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d06047f4c..6319d1f62 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -203,8 +203,11 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, + 'stop_duration': {'type': 'number', 'minimum': 0.0}, + 'trade_limit': {'type': 'number', 'integer': 1}, + 'lookback_period': {'type': 'number', 'integer': 1}, }, - 'required': ['method'], + 'required': ['method', 'trade_limit'], } }, 'telegram': { diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 24f55419b..ed618f6d4 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -15,25 +15,25 @@ class CooldownPeriod(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._stopduration = protection_config.get('stopduration', 60) + self._stop_duration = protection_config.get('stop_duration', 60) def _reason(self) -> str: """ LockReason to use """ - return (f'Cooldown period for {self._stopduration} min.') + return (f'Cooldown period for {self._stop_duration} min.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return (f"{self.name} - Cooldown period of {self._stopduration} min.") + return (f"{self.name} - Cooldown period of {self._stop_duration} min.") def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: """ Get last trade for this pair """ - look_back_until = date_now - timedelta(minutes=self._stopduration) + look_back_until = date_now - timedelta(minutes=self._stop_duration) filters = [ Trade.is_open.is_(False), Trade.close_date > look_back_until, @@ -41,9 +41,9 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stopduration}.") + self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") until = trade.close_date.replace( - tzinfo=timezone.utc) + timedelta(minutes=self._stopduration) + tzinfo=timezone.utc) + timedelta(minutes=self._stop_duration) return True, until, self._reason() return False, None, None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 18888b854..408492063 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -20,14 +20,7 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) - self._stopduration = protection_config.get('stopduration', 60) - - def _reason(self) -> str: - """ - LockReason to use - """ - return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' - f'locking for {self._stopduration} min.') + self._stop_duration = protection_config.get('stop_duration', 60) def short_desc(self) -> str: """ @@ -36,6 +29,13 @@ class StoplossGuard(IProtection): return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " f"within {self._lookback_period} minutes.") + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' + f'locking for {self._stop_duration} min.') + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> ProtectionReturn: """ Evaluate recent trades @@ -55,7 +55,7 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - until = date_now + timedelta(minutes=self._stopduration) + until = date_now + timedelta(minutes=self._stop_duration) return True, until, self._reason() return False, None, None From 00d4820bc108976b759170efa0127e1e7960b5fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 07:49:30 +0100 Subject: [PATCH 099/281] Add low_profit_pairs --- docs/includes/protections.md | 15 ++++ freqtrade/constants.py | 2 +- .../plugins/protections/low_profit_pairs.py | 81 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/protections/low_profit_pairs.py diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 078ba0c2b..aa0ca0f97 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,6 +21,21 @@ Protections will protect your strategy from unexpected events and market conditi !!! Note `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. +#### Low Profit Pairs + +`LowProfitpairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. +If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). + +```json +"protections": [{ + "method": "LowProfitpairs", + "lookback_period": 60, + "trade_limit": 4, + "stop_duration": 60, + "required_profit": 0.02 +}], +``` + ### Full example of Protections The below example stops trading if more than 4 stoploss occur within a 1 hour (60 minute) limit. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6319d1f62..812883da0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod'] +AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitpairs'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py new file mode 100644 index 000000000..739642de7 --- /dev/null +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -0,0 +1,81 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + + +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class LowProfitpairs(IProtection): + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._lookback_period = protection_config.get('lookback_period', 60) + self._trade_limit = protection_config.get('trade_limit', 1) + self._stop_duration = protection_config.get('stop_duration', 60) + self._required_profit = protection_config.get('required_profit', 0.0) + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Low Profit Protection, locks pairs with " + f"profit < {self._required_profit} within {self._lookback_period} minutes.") + + def _reason(self, profit: float) -> str: + """ + LockReason to use + """ + return (f'{profit} < {self._required_profit} in {self._lookback_period} min, ' + f'locking for {self._stop_duration} min.') + + def _low_profit(self, date_now: datetime, pair: str) -> ProtectionReturn: + """ + Evaluate recent trades for pair + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + ] + if pair: + filters.append(Trade.pair == pair) + trades = Trade.get_trades(filters).all() + if len(trades) < self._trade_limit: + # Not enough trades in the relevant period + return False, None, None + + profit = sum(trade.close_profit for trade in trades) + if profit < self._required_profit: + self.log_on_refresh( + logger.info, + f"Trading for {pair} stopped due to {profit} < {self._required_profit} " + f"within {self._lookback_period} minutes.") + until = date_now + timedelta(minutes=self._stop_duration) + return True, until, self._reason(profit) + + return False, None, None + + def global_stop(self, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + return False, None, None + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return self._low_profit(date_now, pair=None) From 1f703dc3419e8a6179363248f6443177b3f86942 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 08:00:10 +0100 Subject: [PATCH 100/281] Improve protection documentation --- docs/includes/protections.md | 62 ++++++++++++++++++++++++++++++++---- freqtrade/constants.py | 2 +- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index aa0ca0f97..8efb02b95 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -8,13 +8,14 @@ Protections will protect your strategy from unexpected events and market conditi #### Stoploss Guard -`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case it will stop trading until this condition is no longer true. +`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. ```json "protections": [{ "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stop_duration": 60 }], ``` @@ -27,25 +28,72 @@ Protections will protect your strategy from unexpected events and market conditi If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). ```json -"protections": [{ +"protections": [ + { "method": "LowProfitpairs", "lookback_period": 60, "trade_limit": 4, "stop_duration": 60, "required_profit": 0.02 -}], + } +], ``` -### Full example of Protections +#### Cooldown Period + +`CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. + -The below example stops trading if more than 4 stoploss occur within a 1 hour (60 minute) limit. ```json "protections": [ + { + "method": "CooldownPeriod", + "stop_duration": 60 + } +], +``` + +!!! Note: + This Protection applies only at pair-level, and will never lock all pairs globally. + +### Full example of Protections + +All protections can be combined at will, also with different parameters, creating a increasing wall for under-performing pairs. +All protections are evaluated in the sequence they are defined. + +The below example: + +* stops trading if more than 4 stoploss occur for all pairs within a 1 hour (60 minute) limit (`StoplossGuard`). +* Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. +* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitpairs`) +* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 7 trades + +```json +"protections": [ + { + "method": "CooldownPeriod", + "stop_duration": 10 + }, { "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stop_duration": 60 + }, + { + "method": "LowProfitpairs", + "lookback_period": 360, + "trade_limit": 4, + "stop_duration": 60, + "required_profit": 0.02 + }, + { + "method": "LowProfitpairs", + "lookback_period": 1440, + "trade_limit": 7, + "stop_duration": 120, + "required_profit": 0.01 } ], ``` diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 812883da0..3f6b6f440 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -207,7 +207,7 @@ CONF_SCHEMA = { 'trade_limit': {'type': 'number', 'integer': 1}, 'lookback_period': {'type': 'number', 'integer': 1}, }, - 'required': ['method', 'trade_limit'], + 'required': ['method'], } }, 'telegram': { From bb06365c503c6c9a9cf5c90f994b588d4568431f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 20:54:24 +0100 Subject: [PATCH 101/281] Improve protection documentation --- docs/includes/protections.md | 29 ++++++++++++------- freqtrade/constants.py | 2 +- .../plugins/protections/low_profit_pairs.py | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 8efb02b95..91b10cf65 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -2,35 +2,46 @@ Protections will protect your strategy from unexpected events and market conditions. +!!! Note + Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. + +!!! Tip + Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). + ### Available Protection Handlers -* [`StoplossGuard`](#stoploss-guard) (default, if not configured differently) +* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. +* [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits +* [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. #### Stoploss Guard `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. ```json -"protections": [{ +"protections": [ + { "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 4, "stop_duration": 60 -}], + } +], ``` !!! Note `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. + `trade_limit` and `lookback_period` will need to be tuned for your strategy. #### Low Profit Pairs -`LowProfitpairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. +`LowProfitPairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). ```json "protections": [ { - "method": "LowProfitpairs", + "method": "LowProfitPairs", "lookback_period": 60, "trade_limit": 4, "stop_duration": 60, @@ -43,8 +54,6 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur `CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. - - ```json "protections": [ { @@ -66,7 +75,7 @@ The below example: * stops trading if more than 4 stoploss occur for all pairs within a 1 hour (60 minute) limit (`StoplossGuard`). * Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. -* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitpairs`) +* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitPairs`) * Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 7 trades ```json @@ -82,14 +91,14 @@ The below example: "stop_duration": 60 }, { - "method": "LowProfitpairs", + "method": "LowProfitPairs", "lookback_period": 360, "trade_limit": 4, "stop_duration": 60, "required_profit": 0.02 }, { - "method": "LowProfitpairs", + "method": "LowProfitPairs", "lookback_period": 1440, "trade_limit": 7, "stop_duration": 120, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3f6b6f440..bc8acc8b3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitpairs'] +AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitPairs'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 739642de7..cbc0052ef 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -11,7 +11,7 @@ from freqtrade.plugins.protections import IProtection, ProtectionReturn logger = logging.getLogger(__name__) -class LowProfitpairs(IProtection): +class LowProfitPairs(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) From 9484ee6690ae15bd8a7e769042db1337d8e2d710 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 21:11:22 +0100 Subject: [PATCH 102/281] Test for low_profit_pairs --- .../plugins/protections/low_profit_pairs.py | 2 +- tests/plugins/test_protections.py | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index cbc0052ef..dc5e1ba24 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -78,4 +78,4 @@ class LowProfitPairs(IProtection): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ - return self._low_profit(date_now, pair=None) + return self._low_profit(date_now, pair=pair) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 59ada7c1e..3417b1a56 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -115,6 +115,56 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() +@pytest.mark.usefixtures("init_persistence") +def test_LowProfitPairs(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "LowProfitPairs", + "lookback_period": 400, + "stopduration": 60, + "trade_limit": 2, + "required_profit": 0.0, + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=800, min_ago_close=450, + )) + + # Not locked with 1 trade + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=120, + )) + + # Not locked with 1 trade (first trade is outside of lookback_period) + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=110, min_ago_close=20, + )) + + # Locks due to 2nd trade + assert not freqtrade.protections.global_stop() + assert freqtrade.protections.stop_per_pair('XRP/BTC') + assert PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " @@ -125,6 +175,11 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", None ), + ({"method": "LowProfitPairs", "stopduration": 60}, + "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " + "profit < 0.0 within 60 minutes.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From 5133675988e3f8e609d0828606af1910f7e264f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Nov 2020 11:41:48 +0100 Subject: [PATCH 103/281] Apply all stops in the list, even if the first would apply already --- freqtrade/plugins/protectionmanager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index b0929af88..64c7208ce 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -48,21 +48,22 @@ class ProtectionManager(): def global_stop(self) -> bool: now = datetime.now(timezone.utc) - + result = False for protection_handler in self._protection_handlers: result, until, reason = protection_handler.global_stop(now) # Early stopping - first positive result blocks further trades if result and until: PairLocks.lock_pair('*', until, reason) - return True - return False + result = True + return result def stop_per_pair(self, pair) -> bool: now = datetime.now(timezone.utc) + result = False for protection_handler in self._protection_handlers: result, until, reason = protection_handler.stop_per_pair(pair, now) if result and until: PairLocks.lock_pair(pair, until, reason) - return True - return False + result = True + return result From 47cd856fea54fbe12f46d25494430d7cf432b2f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Nov 2020 16:18:45 +0100 Subject: [PATCH 104/281] Include protection documentation --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 2e8f6555f..b70a85c04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -91,6 +91,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts +| `protections` | Define one or more protections to be used. [More information below](#protections).
**Datatype:** List of Dicts | `telegram.enabled` | Enable the usage of Telegram.
**Datatype:** Boolean | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String @@ -575,6 +576,7 @@ Assuming both buy and sell are using market orders, a configuration similar to t Obviously, if only one side is using limit orders, different pricing combinations can be used. --8<-- "includes/pairlists.md" +--8<-- "includes/protections.md" ## Switch to Dry-run mode From 59091ef2b774d5e9cc481e0047dbd8db34967156 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Nov 2020 19:43:12 +0100 Subject: [PATCH 105/281] Add helper method to calculate protection until --- freqtrade/freqtradebot.py | 3 +++ freqtrade/plugins/protectionmanager.py | 3 ++- .../plugins/protections/cooldown_period.py | 4 ++-- freqtrade/plugins/protections/iprotection.py | 19 +++++++++++++++++-- .../plugins/protections/low_profit_pairs.py | 4 ++-- .../plugins/protections/stoploss_guard.py | 2 +- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7bfd64c2d..f2ee4d7f0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -182,6 +182,7 @@ class FreqtradeBot: # Evaluate if protections should apply self.protections.global_stop() + # Then looking for buy opportunities if self.get_free_open_trades(): self.enter_positions() @@ -1416,6 +1417,8 @@ class FreqtradeBot: # Updating wallets when order is closed if not trade.is_open: self.protections.stop_per_pair(trade.pair) + # Evaluate if protections should apply + # self.protections.global_stop() self.wallets.update() return False diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 64c7208ce..a79447f02 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -54,7 +54,8 @@ class ProtectionManager(): # Early stopping - first positive result blocks further trades if result and until: - PairLocks.lock_pair('*', until, reason) + if not PairLocks.is_global_lock(until): + PairLocks.lock_pair('*', until, reason) result = True return result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index ed618f6d4..56635984b 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -42,8 +42,8 @@ class CooldownPeriod(IProtection): trade = Trade.get_trades(filters).first() if trade: self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") - until = trade.close_date.replace( - tzinfo=timezone.utc) + timedelta(minutes=self._stop_duration) + until = self.calculate_lock_end([trade], self._stop_duration) + return True, until, self._reason() return False, None, None diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 5dbcf72f6..8048fccf0 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,10 +1,11 @@ import logging from abc import ABC, abstractmethod -from datetime import datetime -from typing import Any, Dict, Optional, Tuple +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, List, Optional, Tuple from freqtrade.mixins import LoggingMixin +from freqtrade.persistence import Trade logger = logging.getLogger(__name__) @@ -45,3 +46,17 @@ class IProtection(LoggingMixin, ABC): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ + + @staticmethod + def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime: + """ + Get lock end time + """ + max_date: datetime = max([trade.close_date for trade in trades]) + # comming from Database, tzinfo is not set. + if max_date.tzinfo is None: + max_date = max_date.replace(tzinfo=timezone.utc) + + until = max_date + timedelta(minutes=stop_minutes) + + return until diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index dc5e1ba24..38d0886bb 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -3,7 +3,6 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict - from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -57,7 +56,8 @@ class LowProfitPairs(IProtection): logger.info, f"Trading for {pair} stopped due to {profit} < {self._required_profit} " f"within {self._lookback_period} minutes.") - until = date_now + timedelta(minutes=self._stop_duration) + until = self.calculate_lock_end(trades, self._stop_duration) + return True, until, self._reason(profit) return False, None, None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 408492063..6335172f8 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -55,7 +55,7 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - until = date_now + timedelta(minutes=self._stop_duration) + until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason() return False, None, None From fc97266dd47011aa49c85d35bb7f194711fd57d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Nov 2020 07:21:59 +0100 Subject: [PATCH 106/281] Add "now" to lock_pair method --- freqtrade/persistence/pairlock_middleware.py | 13 +++++++++++-- freqtrade/plugins/protectionmanager.py | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 44fc228f6..38b5a5d63 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -22,10 +22,19 @@ class PairLocks(): timeframe: str = '' @staticmethod - def lock_pair(pair: str, until: datetime, reason: str = None) -> None: + def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None: + """ + Create PairLock from now to "until". + Uses database by default, unless PairLocks.use_db is set to False, + in which case a list is maintained. + :param pair: pair to lock. use '*' to lock all pairs + :param until: End time of the lock. Will be rounded up to the next candle. + :param reason: Reason string that will be shown as reason for the lock + :param now: Current timestamp. Used to determine lock start time. + """ lock = PairLock( pair=pair, - lock_time=datetime.now(timezone.utc), + lock_time=now or datetime.now(timezone.utc), lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until), reason=reason, active=True diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index a79447f02..33a51970c 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -55,7 +55,7 @@ class ProtectionManager(): # Early stopping - first positive result blocks further trades if result and until: if not PairLocks.is_global_lock(until): - PairLocks.lock_pair('*', until, reason) + PairLocks.lock_pair('*', until, reason, now=now) result = True return result From e29d918ea54af338131b518cbe8ffad012c506a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Nov 2020 08:01:12 +0100 Subject: [PATCH 107/281] Avoid double-locks also in per pair locks --- freqtrade/plugins/protectionmanager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 33a51970c..e58a50c80 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -65,6 +65,7 @@ class ProtectionManager(): for protection_handler in self._protection_handlers: result, until, reason = protection_handler.stop_per_pair(pair, now) if result and until: - PairLocks.lock_pair(pair, until, reason) + if not PairLocks.is_pair_locked(pair, until): + PairLocks.lock_pair(pair, until, reason, now=now) result = True return result From 2e5b9fd4b27bfbbb4c027f8e2e28d88e5677a9b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Nov 2020 08:04:19 +0100 Subject: [PATCH 108/281] format profit in low_profit_pairs --- freqtrade/plugins/protections/low_profit_pairs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 38d0886bb..cc827529f 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -54,7 +54,7 @@ class LowProfitPairs(IProtection): if profit < self._required_profit: self.log_on_refresh( logger.info, - f"Trading for {pair} stopped due to {profit} < {self._required_profit} " + f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " f"within {self._lookback_period} minutes.") until = self.calculate_lock_end(trades, self._stop_duration) From 8ebd6ad2003ca29ba5dac192ce0258cef9e6894d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 19:45:22 +0100 Subject: [PATCH 109/281] Rename login-mixin log method --- freqtrade/mixins/logging_mixin.py | 6 +++--- freqtrade/pairlist/AgeFilter.py | 8 ++++---- freqtrade/pairlist/PrecisionFilter.py | 5 ++--- freqtrade/pairlist/PriceFilter.py | 18 +++++++++--------- freqtrade/pairlist/SpreadFilter.py | 6 +++--- freqtrade/pairlist/VolumePairList.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 9 ++++----- .../plugins/protections/cooldown_period.py | 2 +- .../plugins/protections/low_profit_pairs.py | 2 +- .../plugins/protections/stoploss_guard.py | 4 ++-- tests/plugins/test_pairlist.py | 8 ++++---- 11 files changed, 34 insertions(+), 36 deletions(-) diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index 4e19e45a4..db2307ad3 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -16,7 +16,7 @@ class LoggingMixin(): self.refresh_period = refresh_period self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) - def log_on_refresh(self, logmethod, message: str) -> None: + def log_once(self, logmethod, message: str) -> None: """ Logs message - not more often than "refresh_period" to avoid log spamming Logs the log-message as debug as well to simplify debugging. @@ -25,10 +25,10 @@ class LoggingMixin(): :return: None. """ @cached(cache=self._log_cache) - def _log_on_refresh(message: str): + def _log_once(message: str): logmethod(message) # Log as debug first self.logger.debug(message) # Call hidden function. - _log_on_refresh(message) + _log_once(message) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index e2a13c20a..dd63c1147 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -76,9 +76,9 @@ class AgeFilter(IPairList): self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 return True else: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because age {len(daily_candles)} is less than " - f"{self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}") + self.log_once(logger.info, + f"Removed {ticker['symbol']} from whitelist, because age " + f"{len(daily_candles)} is less than {self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}") return False return False diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 29e32fd44..a28d54205 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -59,9 +59,8 @@ class PrecisionFilter(IPairList): logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: - self.log_on_refresh(logger.info, - f"Removed {ticker['symbol']} from whitelist, " - f"because stop price {sp} would be <= stop limit {stop_gap_price}") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, because " + f"stop price {sp} would be <= stop limit {stop_gap_price}") return False return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index bef1c0a15..a5d73b728 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -64,9 +64,9 @@ class PriceFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None or ticker['last'] == 0: - self.log_on_refresh(logger.info, - f"Removed {ticker['symbol']} from whitelist, because " - "ticker['last'] is empty (Usually no trade in the last 24h).") + self.log_once(logger.info, + f"Removed {ticker['symbol']} from whitelist, because " + "ticker['last'] is empty (Usually no trade in the last 24h).") return False # Perform low_price_ratio check. @@ -74,22 +74,22 @@ class PriceFilter(IPairList): compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") return False # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price < {self._min_price:.8f}") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}") return False # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price > {self._max_price:.8f}") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}") return False return True diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index a636b90bd..963ecb82a 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -45,9 +45,9 @@ class SpreadFilter(IPairList): if 'bid' in ticker and 'ask' in ticker: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because spread {spread * 100:.3f}% >" - f"{self._max_spread_ratio * 100}%") + self.log_once(logger.info, + f"Removed {ticker['symbol']} from whitelist, because spread " + f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%") return False else: return True diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 7d3c2c653..24e1674fd 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -111,6 +111,6 @@ class VolumePairList(IPairList): # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] - self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") + self.log_once(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") return pairs diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index b460ff477..7a1b69a1a 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -78,11 +78,10 @@ class RangeStabilityFilter(IPairList): if pct_change >= self._min_rate_of_change: result = True else: - self.log_on_refresh(logger.info, - f"Removed {pair} from whitelist, " - f"because rate of change over {plural(self._days, 'day')} is " - f"{pct_change:.3f}, which is below the " - f"threshold of {self._min_rate_of_change}.") + self.log_once(logger.info, + f"Removed {pair} from whitelist, because rate of change " + f"over {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is below the threshold of {self._min_rate_of_change}.") result = False self._pair_cache[pair] = result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 56635984b..447ca4363 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -41,7 +41,7 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") + self.log_once(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") until = self.calculate_lock_end([trade], self._stop_duration) return True, until, self._reason() diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index cc827529f..96fb2b08e 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -52,7 +52,7 @@ class LowProfitPairs(IProtection): profit = sum(trade.close_profit for trade in trades) if profit < self._required_profit: - self.log_on_refresh( + self.log_once( logger.info, f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " f"within {self._lookback_period} minutes.") diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 6335172f8..8b6871915 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -53,8 +53,8 @@ class StoplossGuard(IProtection): trades = Trade.get_trades(filters).all() if len(trades) > self._trade_limit: - self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " - f"stoplosses within {self._lookback_period} minutes.") + self.log_once(logger.info, f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.") until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason() diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 1d2f16b45..2f1617f6c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -92,7 +92,7 @@ def static_pl_conf(whitelist_conf): return whitelist_conf -def test_log_on_refresh(mocker, static_pl_conf, markets, tickers): +def test_log_cached(mocker, static_pl_conf, markets, tickers): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), @@ -102,14 +102,14 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers): logmock = MagicMock() # Assign starting whitelist pl = freqtrade.pairlists._pairlist_handlers[0] - pl.log_on_refresh(logmock, 'Hello world') + pl.log_once(logmock, 'Hello world') assert logmock.call_count == 1 - pl.log_on_refresh(logmock, 'Hello world') + pl.log_once(logmock, 'Hello world') assert logmock.call_count == 1 assert pl._log_cache.currsize == 1 assert ('Hello world',) in pl._log_cache._Cache__data - pl.log_on_refresh(logmock, 'Hello world2') + pl.log_once(logmock, 'Hello world2') assert logmock.call_count == 2 assert pl._log_cache.currsize == 2 From 2cd54a59333feafbf64a664fe151a199e91a8ce4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 20:06:13 +0100 Subject: [PATCH 110/281] Allow disabling output from plugins --- freqtrade/mixins/logging_mixin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index db2307ad3..a8dec2da7 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -8,6 +8,9 @@ class LoggingMixin(): Logging Mixin Shows similar messages only once every `refresh_period`. """ + # Disable output completely + show_output = True + def __init__(self, logger, refresh_period: int = 3600): """ :param refresh_period: in seconds - Show identical messages in this intervals @@ -31,4 +34,5 @@ class LoggingMixin(): # Log as debug first self.logger.debug(message) # Call hidden function. - _log_once(message) + if self.show_output: + _log_once(message) From 5e3d2401f5957de2cbea427f671cb4b013c0e1b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 20:34:29 +0100 Subject: [PATCH 111/281] Only call stop methods when they actually support this method --- freqtrade/plugins/protectionmanager.py | 24 ++++++++++--------- .../plugins/protections/cooldown_period.py | 5 ++++ freqtrade/plugins/protections/iprotection.py | 7 +++++- .../plugins/protections/low_profit_pairs.py | 5 ++++ .../plugins/protections/stoploss_guard.py | 9 +++++-- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index e58a50c80..d12f4ba80 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -50,22 +50,24 @@ class ProtectionManager(): now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: - result, until, reason = protection_handler.global_stop(now) + if protection_handler.has_global_stop: + result, until, reason = protection_handler.global_stop(now) - # Early stopping - first positive result blocks further trades - if result and until: - if not PairLocks.is_global_lock(until): - PairLocks.lock_pair('*', until, reason, now=now) - result = True + # Early stopping - first positive result blocks further trades + if result and until: + if not PairLocks.is_global_lock(until): + PairLocks.lock_pair('*', until, reason, now=now) + result = True return result def stop_per_pair(self, pair) -> bool: now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: - result, until, reason = protection_handler.stop_per_pair(pair, now) - if result and until: - if not PairLocks.is_pair_locked(pair, until): - PairLocks.lock_pair(pair, until, reason, now=now) - result = True + if protection_handler.has_local_stop: + result, until, reason = protection_handler.stop_per_pair(pair, now) + if result and until: + if not PairLocks.is_pair_locked(pair, until): + PairLocks.lock_pair(pair, until, reason, now=now) + result = True return result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 447ca4363..4fe0a4fdc 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -12,6 +12,11 @@ logger = logging.getLogger(__name__) class CooldownPeriod(IProtection): + # Can globally stop the bot + has_global_stop: bool = False + # Can stop trading for one pair + has_local_stop: bool = True + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 8048fccf0..49fccb0e6 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,6 +1,6 @@ import logging -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod, abstractproperty from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple @@ -15,6 +15,11 @@ ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str]] class IProtection(LoggingMixin, ABC): + # Can globally stop the bot + has_global_stop: bool = False + # Can stop trading for one pair + has_local_stop: bool = False + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 96fb2b08e..48efa3c9a 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -12,6 +12,11 @@ logger = logging.getLogger(__name__) class LowProfitPairs(IProtection): + # Can globally stop the bot + has_global_stop: bool = False + # Can stop trading for one pair + has_local_stop: bool = True + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 8b6871915..51a2fded8 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -15,6 +15,11 @@ logger = logging.getLogger(__name__) class StoplossGuard(IProtection): + # Can globally stop the bot + has_global_stop: bool = True + # Can stop trading for one pair + has_local_stop: bool = True + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) @@ -67,7 +72,7 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, all pairs will be locked with until """ - return self._stoploss_guard(date_now, pair=None) + return self._stoploss_guard(date_now, None) def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: """ @@ -76,4 +81,4 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ - return False, None, None + return self._stoploss_guard(date_now, pair) From be57ceb2526a57cfda4ec209fbbf0d8efc358381 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 14:46:27 +0100 Subject: [PATCH 112/281] Remove confusing entry (in this branch of the if statement, candle_date is empty --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 81f4e7651..d14d3a35f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -312,7 +312,7 @@ class IStrategy(ABC): if not candle_date: # Simple call ... - return PairLocks.is_pair_locked(pair, candle_date) + return PairLocks.is_pair_locked(pair) else: lock_time = timeframe_to_next_date(self.timeframe, candle_date) return PairLocks.is_pair_locked(pair, lock_time) From 8d9c66a638af5fa57d11bc5dde8563ebf2ede984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 11:41:09 +0100 Subject: [PATCH 113/281] Add LogginMixin to freqtradebot class to avoid over-logging --- freqtrade/freqtradebot.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2ee4d7f0..24827a7e3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,8 +19,9 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 +from freqtrade.mixins import LoggingMixin from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.plugins.protectionmanager import ProtectionManager @@ -35,7 +36,7 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) -class FreqtradeBot: +class FreqtradeBot(LoggingMixin): """ Freqtrade is the main class of the bot. This is from here the bot start its logic. @@ -104,6 +105,7 @@ class FreqtradeBot: self.rpc: RPCManager = RPCManager(self) # Protect sell-logic from forcesell and viceversa self._sell_lock = Lock() + LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) def notify_status(self, msg: str) -> None: """ @@ -365,7 +367,7 @@ class FreqtradeBot: "but checking to sell open trades.") return trades_created if PairLocks.is_global_lock(): - logger.info("Global pairlock active. Not creating new trades.") + self.log_once(logger.info, "Global pairlock active. Not creating new trades.") return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -551,7 +553,7 @@ class FreqtradeBot: analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) if self.strategy.is_pair_locked( pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): - logger.info(f"Pair {pair} is currently locked.") + self.log_once(logger.info, f"Pair {pair} is currently locked.") return False # get_free_open_trades is checked before create_trade is called From 8f958ef7238dcd1fa3046ce7307873d87ad85a54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 11:49:41 +0100 Subject: [PATCH 114/281] Improve login-mixin structure --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/mixins/logging_mixin.py | 5 +++-- freqtrade/pairlist/AgeFilter.py | 5 ++--- freqtrade/pairlist/PrecisionFilter.py | 4 ++-- freqtrade/pairlist/PriceFilter.py | 18 +++++++++--------- freqtrade/pairlist/SpreadFilter.py | 6 +++--- freqtrade/pairlist/VolumePairList.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 6 +++--- .../plugins/protections/cooldown_period.py | 4 ++-- freqtrade/plugins/protections/iprotection.py | 2 +- .../plugins/protections/low_profit_pairs.py | 3 +-- .../plugins/protections/stoploss_guard.py | 4 ++-- tests/plugins/test_pairlist.py | 6 +++--- tests/plugins/test_protections.py | 1 + 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 24827a7e3..265a8ce10 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -367,7 +367,7 @@ class FreqtradeBot(LoggingMixin): "but checking to sell open trades.") return trades_created if PairLocks.is_global_lock(): - self.log_once(logger.info, "Global pairlock active. Not creating new trades.") + self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -553,7 +553,7 @@ class FreqtradeBot(LoggingMixin): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) if self.strategy.is_pair_locked( pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): - self.log_once(logger.info, f"Pair {pair} is currently locked.") + self.log_once(f"Pair {pair} is currently locked.", logger.info) return False # get_free_open_trades is checked before create_trade is called diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index a8dec2da7..e9921e1ec 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -1,5 +1,6 @@ +from typing import Callable from cachetools import TTLCache, cached @@ -19,12 +20,12 @@ class LoggingMixin(): self.refresh_period = refresh_period self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) - def log_once(self, logmethod, message: str) -> None: + def log_once(self, message: str, logmethod: Callable) -> None: """ Logs message - not more often than "refresh_period" to avoid log spamming Logs the log-message as debug as well to simplify debugging. - :param logmethod: Function that'll be called. Most likely `logger.info`. :param message: String containing the message to be sent to the function. + :param logmethod: Function that'll be called. Most likely `logger.info`. :return: None. """ @cached(cache=self._log_cache) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index dd63c1147..ae2132637 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -76,9 +76,8 @@ class AgeFilter(IPairList): self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 return True else: - self.log_once(logger.info, - f"Removed {ticker['symbol']} from whitelist, because age " + self.log_once(f"Removed {ticker['symbol']} from whitelist, because age " f"{len(daily_candles)} is less than {self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}") + f"{plural(self._min_days_listed, 'day')}", logger.info) return False return False diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index a28d54205..db05d5883 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -59,8 +59,8 @@ class PrecisionFilter(IPairList): logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, because " - f"stop price {sp} would be <= stop limit {stop_gap_price}") + self.log_once(f"Removed {ticker['symbol']} from whitelist, because " + f"stop price {sp} would be <= stop limit {stop_gap_price}", logger.info) return False return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index a5d73b728..3686cd138 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -64,9 +64,9 @@ class PriceFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None or ticker['last'] == 0: - self.log_once(logger.info, - f"Removed {ticker['symbol']} from whitelist, because " - "ticker['last'] is empty (Usually no trade in the last 24h).") + self.log_once(f"Removed {ticker['symbol']} from whitelist, because " + "ticker['last'] is empty (Usually no trade in the last 24h).", + logger.info) return False # Perform low_price_ratio check. @@ -74,22 +74,22 @@ class PriceFilter(IPairList): compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") + self.log_once(f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price < {self._min_price:.8f}") + self.log_once(f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}", logger.info) return False # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price > {self._max_price:.8f}") + self.log_once(f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}", logger.info) return False return True diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 963ecb82a..6c4e9f12f 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -45,9 +45,9 @@ class SpreadFilter(IPairList): if 'bid' in ticker and 'ask' in ticker: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: - self.log_once(logger.info, - f"Removed {ticker['symbol']} from whitelist, because spread " - f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%") + self.log_once(f"Removed {ticker['symbol']} from whitelist, because spread " + f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%", + logger.info) return False else: return True diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 24e1674fd..7056bc59d 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -111,6 +111,6 @@ class VolumePairList(IPairList): # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] - self.log_once(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") + self.log_once(f"Searching {self._number_pairs} pairs: {pairs}", logger.info) return pairs diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 7a1b69a1a..756368355 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -78,10 +78,10 @@ class RangeStabilityFilter(IPairList): if pct_change >= self._min_rate_of_change: result = True else: - self.log_once(logger.info, - f"Removed {pair} from whitelist, because rate of change " + self.log_once(f"Removed {pair} from whitelist, because rate of change " f"over {plural(self._days, 'day')} is {pct_change:.3f}, " - f"which is below the threshold of {self._min_rate_of_change}.") + f"which is below the threshold of {self._min_rate_of_change}.", + logger.info) result = False self._pair_cache[pair] = result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 4fe0a4fdc..1abec7218 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,6 +1,6 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from typing import Any, Dict from freqtrade.persistence import Trade @@ -46,7 +46,7 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_once(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") + self.log_once(f"Cooldown for {pair} for {self._stop_duration}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) return True, until, self._reason() diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 49fccb0e6..0f539bbd3 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,6 +1,6 @@ import logging -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 48efa3c9a..c45ba3a39 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -58,9 +58,8 @@ class LowProfitPairs(IProtection): profit = sum(trade.close_profit for trade in trades) if profit < self._required_profit: self.log_once( - logger.info, f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " - f"within {self._lookback_period} minutes.") + f"within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(profit) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 51a2fded8..0645d366b 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -58,8 +58,8 @@ class StoplossGuard(IProtection): trades = Trade.get_trades(filters).all() if len(trades) > self._trade_limit: - self.log_once(logger.info, f"Trading stopped due to {self._trade_limit} " - f"stoplosses within {self._lookback_period} minutes.") + self.log_once(f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason() diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 2f1617f6c..c2a4a69d7 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -102,14 +102,14 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers): logmock = MagicMock() # Assign starting whitelist pl = freqtrade.pairlists._pairlist_handlers[0] - pl.log_once(logmock, 'Hello world') + pl.log_once('Hello world', logmock) assert logmock.call_count == 1 - pl.log_once(logmock, 'Hello world') + pl.log_once('Hello world', logmock) assert logmock.call_count == 1 assert pl._log_cache.currsize == 1 assert ('Hello world',) in pl._log_cache._Cache__data - pl.log_once(logmock, 'Hello world2') + pl.log_once('Hello world2', logmock) assert logmock.call_count == 2 assert pl._log_cache.currsize == 2 diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 3417b1a56..1a22d08a2 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -165,6 +165,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " From 32cde1cb7da970b3dde7874db35c57984f442409 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 10:48:54 +0100 Subject: [PATCH 115/281] Improve test for lowprofitpairs --- tests/plugins/test_protections.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 1a22d08a2..a02a0366c 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -11,6 +11,7 @@ from tests.conftest import get_patched_freqtradebot, log_has_re def generate_mock_trade(pair: str, fee: float, is_open: bool, sell_reason: str = SellType.SELL_SIGNAL, min_ago_open: int = None, min_ago_close: int = None, + profit_rate: float = 0.9 ): open_rate = random.random() @@ -28,8 +29,9 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, ) trade.recalc_open_trade_price() if not is_open: - trade.close(open_rate * (1 - 0.9)) + trade.close(open_rate * profit_rate) trade.sell_reason = sell_reason + return trade @@ -134,7 +136,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, - min_ago_open=800, min_ago_close=450, + min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) # Not locked with 1 trade @@ -145,7 +147,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, - min_ago_open=200, min_ago_close=120, + min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) # Not locked with 1 trade (first trade is outside of lookback_period) @@ -154,9 +156,17 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() + # Add positive trade + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=20, min_ago_close=10, profit_rate=1.15, + )) + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, - min_ago_open=110, min_ago_close=20, + min_ago_open=110, min_ago_close=20, profit_rate=0.8, )) # Locks due to 2nd trade @@ -166,6 +176,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " From dcdf4a0503281c02598b2d171c0e5f06f7878e15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 10:58:50 +0100 Subject: [PATCH 116/281] Improve tests --- tests/plugins/test_protections.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index a02a0366c..ce0ad7d5e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -5,6 +5,7 @@ import pytest from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.interface import SellType +from freqtrade import constants from tests.conftest import get_patched_freqtradebot, log_has_re @@ -35,6 +36,19 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, return trade +def test_protectionmanager(mocker, default_conf): + default_conf['protections'] = [{'method': protection} + for protection in constants.AVAILABLE_PROTECTIONS] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + for handler in freqtrade.protections._protection_handlers: + assert handler.name in constants.AVAILABLE_PROTECTIONS + if not handler.has_global_stop: + assert handler.global_stop(datetime.utcnow()) == (False, None, None) + if not handler.has_local_stop: + assert handler.local_stop('XRP/BTC', datetime.utcnow()) == (False, None, None) + + @pytest.mark.usefixtures("init_persistence") def test_stoploss_guard(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ @@ -176,7 +190,6 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() - @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " From dce236467224f4d80ce7c6c90927796fdff54722 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 11:11:55 +0100 Subject: [PATCH 117/281] Add stoploss per pair support --- docs/includes/protections.md | 4 +- .../plugins/protections/stoploss_guard.py | 3 + tests/plugins/test_protections.py | 58 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 91b10cf65..644b98e64 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -17,6 +17,7 @@ Protections will protect your strategy from unexpected events and market conditi #### Stoploss Guard `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. ```json "protections": [ @@ -24,7 +25,8 @@ Protections will protect your strategy from unexpected events and market conditi "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 4, - "stop_duration": 60 + "stop_duration": 60, + "only_per_pair": false } ], ``` diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 0645d366b..1ad839f3d 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -26,6 +26,7 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) self._stop_duration = protection_config.get('stop_duration', 60) + self._disable_global_stop = protection_config.get('only_per_pair', False) def short_desc(self) -> str: """ @@ -72,6 +73,8 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, all pairs will be locked with until """ + if self._disable_global_stop: + return False, None, None return self._stoploss_guard(date_now, None) def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index ce0ad7d5e..7eac737ef 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -95,6 +95,64 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert PairLocks.is_global_lock() +@pytest.mark.parametrize('only_per_pair', [False, True]) +@pytest.mark.usefixtures("init_persistence") +def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair): + default_conf['protections'] = [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 1, + "only_per_pair": only_per_pair + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + pair = 'XRP/BTC' + assert not freqtrade.protections.stop_per_pair(pair) + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, profit_rate=0.9, + )) + + assert not freqtrade.protections.stop_per_pair(pair) + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + # This trade does not count, as it's closed too long ago + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=250, min_ago_close=100, profit_rate=0.9, + )) + # Trade does not count for per pair stop as it's the wrong pair. + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=240, min_ago_close=30, profit_rate=0.9, + )) + # 3 Trades closed - but the 2nd has been closed too long ago. + assert not freqtrade.protections.stop_per_pair(pair) + assert freqtrade.protections.global_stop() != only_per_pair + if not only_per_pair: + assert log_has_re(message, caplog) + else: + assert not log_has_re(message, caplog) + + caplog.clear() + + # 2nd Trade that counts with correct pair + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=180, min_ago_close=30, profit_rate=0.9, + )) + + assert freqtrade.protections.stop_per_pair(pair) + assert freqtrade.protections.global_stop() != only_per_pair + assert PairLocks.is_pair_locked(pair) + assert PairLocks.is_global_lock() != only_per_pair + + @pytest.mark.usefixtures("init_persistence") def test_CooldownPeriod(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ From 6d0f16920f47961f9bae3d3be0e316fbbf368bea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 11:54:11 +0100 Subject: [PATCH 118/281] Get Longest lock logic --- freqtrade/freqtradebot.py | 20 +++++++++--- freqtrade/persistence/pairlock_middleware.py | 11 ++++++- tests/plugins/test_pairlocks.py | 32 ++++++++++++++++++++ tests/test_freqtradebot.py | 8 ++--- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 265a8ce10..1e0f5fdf0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -367,7 +367,13 @@ class FreqtradeBot(LoggingMixin): "but checking to sell open trades.") return trades_created if PairLocks.is_global_lock(): - self.log_once("Global pairlock active. Not creating new trades.", logger.info) + lock = PairLocks.get_pair_longest_lock('*') + if lock: + self.log_once(f"Global pairlock active until " + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " + "Not creating new trades.", logger.info) + else: + self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -551,9 +557,15 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"create_trade for pair {pair}") analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) - if self.strategy.is_pair_locked( - pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): - self.log_once(f"Pair {pair} is currently locked.", logger.info) + nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None + if self.strategy.is_pair_locked(pair, nowtime): + lock = PairLocks.get_pair_longest_lock(pair, nowtime) + if lock: + self.log_once(f"Pair {pair} is still locked until " + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}.", + logger.info) + else: + self.log_once(f"Pair {pair} is still locked.", logger.info) return False # get_free_open_trades is checked before create_trade is called diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 38b5a5d63..de804f025 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -46,7 +46,7 @@ class PairLocks(): PairLocks.locks.append(lock) @staticmethod - def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: + def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -66,6 +66,15 @@ class PairLocks(): )] return locks + @staticmethod + def get_pair_longest_lock(pair: str, now: Optional[datetime] = None) -> Optional[PairLock]: + """ + Get the lock that expires the latest for the pair given. + """ + locks = PairLocks.get_pair_locks(pair, now) + locks = sorted(locks, key=lambda l: l.lock_end_time, reverse=True) + return locks[0] if locks else None + @staticmethod def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: """ diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index 0b6b89717..db7d9f46f 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -80,3 +80,35 @@ def test_PairLocks(use_db): assert len(PairLock.query.all()) == 0 # Reset use-db variable PairLocks.use_db = True + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks_getlongestlock(use_db): + PairLocks.timeframe = '5m' + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + else: + PairLocks.use_db = False + + assert PairLocks.use_db == use_db + + pair = 'ETH/BTC' + assert not PairLocks.is_pair_locked(pair) + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLocks.is_pair_locked(pair) + lock = PairLocks.get_pair_longest_lock(pair) + + assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=3) + assert lock.lock_end_time.replace(tzinfo=timezone.utc) < arrow.utcnow().shift(minutes=14) + + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=15).datetime) + assert PairLocks.is_pair_locked(pair) + + lock = PairLocks.get_pair_longest_lock(pair) + # Must be longer than above + assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=14) + + PairLocks.use_db = True diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 94ed06cd9..142729f4d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -692,16 +692,16 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - message = "Global pairlock active. Not creating new trades." + message = r"Global pairlock active until.* Not creating new trades." n = freqtrade.enter_positions() # 0 trades, but it's not because of pairlock. assert n == 0 - assert not log_has(message, caplog) + assert not log_has_re(message, caplog) PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because') n = freqtrade.enter_positions() assert n == 0 - assert log_has(message, caplog) + assert log_has_re(message, caplog) def test_create_trade_no_signal(default_conf, fee, mocker) -> None: @@ -3289,7 +3289,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo caplog.clear() freqtrade.enter_positions() - assert log_has(f"Pair {trade.pair} is currently locked.", caplog) + assert log_has_re(f"Pair {trade.pair} is still locked.*", caplog) def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, From 12e84bda1e1940333ba8fb649d289c0fd5303a98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 10:29:45 +0100 Subject: [PATCH 119/281] Add developer docs for Protections --- docs/developer.md | 47 +++++++++++++++++++++++++++++++++++- docs/includes/protections.md | 2 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 662905d65..86e9b1078 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -94,7 +94,7 @@ Below is an outline of exception inheritance hierarchy: +---+ StrategyError ``` -## Modules +## Plugins ### Pairlists @@ -173,6 +173,51 @@ In `VolumePairList`, this implements different methods of sorting, does early va return pairs ``` +### Protections + +Best read the [Protection documentation](configuration.md#protections) to understand protections. +This Guide is directed towards Developers who want to develop a new protection. + +No protection should use datetime directly, but use the provided `date_now` variable for date calculations. This preserves the ability to backtest protections. + +!!! Tip "Writing a new Protection" + Best copy one of the existing Protections to have a good example. + +#### Implementation of a new protection + +All Protection implementations must have `IProtection` as parent class. +For that reason, they must implement the following methods: + +* `short_desc()` +* `global_stop()` +* `stop_per_pair()`. + +`global_stop()` and `stop_per_pair()` must return a ProtectionReturn tuple, which consists of: + +* lock pair - boolean +* lock until - datetime - until when should the pair be locked (will be rounded up to the next new candle) +* reason - string, used for logging and storage in the database + +The `until` portion should be calculated using the provided `calculate_lock_end()` method. + +#### Global vs. local stops + +Protections can have 2 different ways to stop trading for a limited : + +* Per pair (local) +* For all Pairs (globally) + +##### Protections - per pair + +Protections that implement the per pair approach must set `has_local_stop=True`. +The method `stop_per_pair()` will be called once, whenever a sell order is closed, and the trade is therefore closed. + +##### Protections - global protection + +These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock). +Global protection must set `has_global_stop=True` to be evaluated for global stops. +The method `global_stop()` will be called on every iteration, so they should not do too heavy calculations (or should cache the calculations across runs). + ## Implement a new Exchange (WIP) !!! Note diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 644b98e64..aaf5bbff4 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,6 +1,6 @@ ## Protections -Protections will protect your strategy from unexpected events and market conditions. +Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. !!! Note Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. From 4351a26b4cc84abfab5b0fd901c60918d488175e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 10:32:23 +0100 Subject: [PATCH 120/281] Move stop_duration to parent class avoids reimplementation and enhances standardization --- docs/developer.md | 3 +++ freqtrade/plugins/protections/cooldown_period.py | 2 -- freqtrade/plugins/protections/iprotection.py | 2 ++ freqtrade/plugins/protections/low_profit_pairs.py | 1 - freqtrade/plugins/protections/stoploss_guard.py | 1 - 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 86e9b1078..ebfe8e013 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -200,6 +200,9 @@ For that reason, they must implement the following methods: The `until` portion should be calculated using the provided `calculate_lock_end()` method. +All Protections should use `"stop_duration"` to define how long a a pair (or all pairs) should be locked. +The content of this is made available as `self._stop_duration` to the each Protection. + #### Global vs. local stops Protections can have 2 different ways to stop trading for a limited : diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 1abec7218..18a73ef5b 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -20,8 +20,6 @@ class CooldownPeriod(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._stop_duration = protection_config.get('stop_duration', 60) - def _reason(self) -> str: """ LockReason to use diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 0f539bbd3..2053ae741 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -23,6 +23,8 @@ class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config + self._stop_duration = protection_config.get('stop_duration', 60) + LoggingMixin.__init__(self, logger) @property diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index c45ba3a39..cd850ca0c 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -22,7 +22,6 @@ class LowProfitPairs(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 1) - self._stop_duration = protection_config.get('stop_duration', 60) self._required_profit = protection_config.get('required_profit', 0.0) def short_desc(self) -> str: diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 1ad839f3d..65403d683 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -25,7 +25,6 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) - self._stop_duration = protection_config.get('stop_duration', 60) self._disable_global_stop = protection_config.get('only_per_pair', False) def short_desc(self) -> str: From 397a15cb617ffb668007632cb6cb9cc3c8717639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:14:28 +0100 Subject: [PATCH 121/281] Improve protection documentation --- docs/includes/protections.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index aaf5bbff4..9722e70aa 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,6 +1,7 @@ ## Protections Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. +All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. !!! Note Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. From ad746627b339d243b4bcd9a15a1a54d4a4cb3051 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:47:15 +0100 Subject: [PATCH 122/281] Fix lock-loop --- docs/developer.md | 4 ++-- freqtrade/freqtradebot.py | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index ebfe8e013..6ea641edd 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -213,13 +213,13 @@ Protections can have 2 different ways to stop trading for a limited : ##### Protections - per pair Protections that implement the per pair approach must set `has_local_stop=True`. -The method `stop_per_pair()` will be called once, whenever a sell order is closed, and the trade is therefore closed. +The method `stop_per_pair()` will be called whenever a trade closed (sell order completed). ##### Protections - global protection These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock). Global protection must set `has_global_stop=True` to be evaluated for global stops. -The method `global_stop()` will be called on every iteration, so they should not do too heavy calculations (or should cache the calculations across runs). +The method `global_stop()` will be called whenever a trade closed (sell order completed). ## Implement a new Exchange (WIP) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1e0f5fdf0..ecc824a86 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -182,9 +182,6 @@ class FreqtradeBot(LoggingMixin): # First process current opened trades (positions) self.exit_positions(trades) - # Evaluate if protections should apply - self.protections.global_stop() - # Then looking for buy opportunities if self.get_free_open_trades(): self.enter_positions() @@ -1431,8 +1428,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: self.protections.stop_per_pair(trade.pair) - # Evaluate if protections should apply - # self.protections.global_stop() + self.protections.global_stop() self.wallets.update() return False From 9947dcd1da1660efed3d676c4e537f9c5bd0d045 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:51:58 +0100 Subject: [PATCH 123/281] Beta feature warning --- docs/includes/protections.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 9722e70aa..716addb55 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,5 +1,8 @@ ## Protections +!!! Warning "Beta feature" + This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Issue. + Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. From 768d7fa1966e3694ac97daba57833db8d3c08809 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 18:02:34 +0100 Subject: [PATCH 124/281] Readd optional for get_pair_locks - it's necessary --- freqtrade/persistence/pairlock_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index de804f025..6ce91ee6b 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -46,7 +46,7 @@ class PairLocks(): PairLocks.locks.append(lock) @staticmethod - def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List[PairLock]: + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty From 9d6f3a89ef26081ecab03c3455c9cfa4063ad856 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Nov 2020 11:36:16 +0100 Subject: [PATCH 125/281] Improve docs and fix typos --- docs/developer.md | 11 +++++++++++ freqtrade/constants.py | 4 ++-- tests/plugins/test_protections.py | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 6ea641edd..05b518184 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -94,6 +94,8 @@ Below is an outline of exception inheritance hierarchy: +---+ StrategyError ``` +--- + ## Plugins ### Pairlists @@ -203,6 +205,8 @@ The `until` portion should be calculated using the provided `calculate_lock_end( All Protections should use `"stop_duration"` to define how long a a pair (or all pairs) should be locked. The content of this is made available as `self._stop_duration` to the each Protection. +If your protection requires a look-back period, please use `"lookback_period"` to keep different protections aligned. + #### Global vs. local stops Protections can have 2 different ways to stop trading for a limited : @@ -221,6 +225,13 @@ These Protections should do their evaluation across all pairs, and consequently Global protection must set `has_global_stop=True` to be evaluated for global stops. The method `global_stop()` will be called whenever a trade closed (sell order completed). +##### Protections - calculating lock end time + +Protections should calculate the lock end time based on the last trade it considers. +This avoids relocking should the lookback-period be longer than the actual lock period. + +--- + ## Implement a new Exchange (WIP) !!! Note diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bc8acc8b3..add9aae95 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -204,8 +204,8 @@ CONF_SCHEMA = { 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, 'stop_duration': {'type': 'number', 'minimum': 0.0}, - 'trade_limit': {'type': 'number', 'integer': 1}, - 'lookback_period': {'type': 'number', 'integer': 1}, + 'trade_limit': {'type': 'number', 'minimum': 1}, + 'lookback_period': {'type': 'number', 'minimum': 1}, }, 'required': ['method'], } diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 7eac737ef..24594c3ac 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -54,6 +54,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "StoplossGuard", "lookback_period": 60, + "stop_duration": 40, "trade_limit": 2 }] freqtrade = get_patched_freqtradebot(mocker, default_conf) From 089c463cfb04404a8ea2592dddaf489ed78615cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 08:05:48 +0100 Subject: [PATCH 126/281] Introduce max_drawdown protection --- freqtrade/constants.py | 2 +- .../plugins/protections/cooldown_period.py | 2 - .../plugins/protections/low_profit_pairs.py | 2 - .../protections/max_drawdown_protection.py | 91 +++++++++++++++++++ .../plugins/protections/stoploss_guard.py | 2 - 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 freqtrade/plugins/protections/max_drawdown_protection.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index add9aae95..dfc21b678 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitPairs'] +AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 18a73ef5b..7b37b2303 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -12,9 +12,7 @@ logger = logging.getLogger(__name__) class CooldownPeriod(IProtection): - # Can globally stop the bot has_global_stop: bool = False - # Can stop trading for one pair has_local_stop: bool = True def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index cd850ca0c..515f81521 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -12,9 +12,7 @@ logger = logging.getLogger(__name__) class LowProfitPairs(IProtection): - # Can globally stop the bot has_global_stop: bool = False - # Can stop trading for one pair has_local_stop: bool = True def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py new file mode 100644 index 000000000..e8a920908 --- /dev/null +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -0,0 +1,91 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +import pandas as pd + +from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class MaxDrawdown(IProtection): + + has_global_stop: bool = True + has_local_stop: bool = False + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._lookback_period = protection_config.get('lookback_period', 60) + self._trade_limit = protection_config.get('trade_limit', 1) + self._max_allowed_drawdown = protection_config.get('max_allowed_drawdown', 0.0) + # TODO: Implement checks to limit max_drawdown to sensible values + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Max drawdown protection, stop trading if drawdown is > " + f"{self._max_allowed_drawdown} within {self._lookback_period} minutes.") + + def _reason(self, drawdown: float) -> str: + """ + LockReason to use + """ + return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' + f'locking for {self._stop_duration} min.') + + def _max_drawdown(self, date_now: datetime, pair: str) -> ProtectionReturn: + """ + Evaluate recent trades for drawdown ... + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + ] + if pair: + filters.append(Trade.pair == pair) + trades = Trade.get_trades(filters).all() + + trades_df = pd.DataFrame(trades) + + if len(trades) < self._trade_limit: + # Not enough trades in the relevant period + return False, None, None + + # Drawdown is always positive + drawdown, _, _ = calculate_max_drawdown(trades_df) + + if drawdown > self._max_allowed_drawdown: + self.log_once( + f"Trading for {pair} stopped due to {drawdown:.2f} < {self._max_allowed_drawdown} " + f"within {self._lookback_period} minutes.", logger.info) + until = self.calculate_lock_end(trades, self._stop_duration) + + return True, until, self._reason(drawdown) + + return False, None, None + + def global_stop(self, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + return self._max_drawdown(date_now) + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return False, None, None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 65403d683..b6f430085 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -15,9 +15,7 @@ logger = logging.getLogger(__name__) class StoplossGuard(IProtection): - # Can globally stop the bot has_global_stop: bool = True - # Can stop trading for one pair has_local_stop: bool = True def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: From f06b58dc91d946f4b929531ad4f01ba5754860db Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 19:07:39 +0100 Subject: [PATCH 127/281] Test MaxDrawdown desc --- .../protections/max_drawdown_protection.py | 12 ++-- tests/plugins/test_protections.py | 72 ++++++++++++++++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e8a920908..e5625733c 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -40,7 +40,7 @@ class MaxDrawdown(IProtection): return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' f'locking for {self._stop_duration} min.') - def _max_drawdown(self, date_now: datetime, pair: str) -> ProtectionReturn: + def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: """ Evaluate recent trades for drawdown ... """ @@ -49,23 +49,21 @@ class MaxDrawdown(IProtection): Trade.is_open.is_(False), Trade.close_date > look_back_until, ] - if pair: - filters.append(Trade.pair == pair) trades = Trade.get_trades(filters).all() - trades_df = pd.DataFrame(trades) + trades_df = pd.DataFrame([trade.to_json() for trade in trades]) if len(trades) < self._trade_limit: # Not enough trades in the relevant period return False, None, None # Drawdown is always positive - drawdown, _, _ = calculate_max_drawdown(trades_df) + drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') if drawdown > self._max_allowed_drawdown: self.log_once( - f"Trading for {pair} stopped due to {drawdown:.2f} < {self._max_allowed_drawdown} " - f"within {self._lookback_period} minutes.", logger.info) + f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" + f" within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(drawdown) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 24594c3ac..e5bbec431 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -46,7 +46,7 @@ def test_protectionmanager(mocker, default_conf): if not handler.has_global_stop: assert handler.global_stop(datetime.utcnow()) == (False, None, None) if not handler.has_local_stop: - assert handler.local_stop('XRP/BTC', datetime.utcnow()) == (False, None, None) + assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) @pytest.mark.usefixtures("init_persistence") @@ -249,6 +249,71 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "MaxDrawdown", + "lookback_period": 1000, + "stopduration": 60, + "trade_limit": 3, + "max_allowed_drawdown": 0.15 + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to Max.*" + + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=500, min_ago_close=400, profit_rate=0.9, + )) + # Not locked with one trade + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, + )) + + # Not locked with 1 trade (2nd trade is outside of lookback_period) + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + assert not log_has_re(message, caplog) + + # Winning trade ... (should not lock, does not change drawdown!) + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=320, min_ago_close=410, profit_rate=1.5, + )) + assert not freqtrade.protections.global_stop() + assert not PairLocks.is_global_lock() + + caplog.clear() + + # Add additional negative trade, causing a loss of > 15% + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=20, min_ago_close=10, profit_rate=0.8, + )) + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + # local lock not supported + assert not PairLocks.is_pair_locked('XRP/BTC') + assert freqtrade.protections.global_stop() + assert PairLocks.is_global_lock() + assert log_has_re(message, caplog) + + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " @@ -264,6 +329,11 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): "profit < 0.0 within 60 minutes.'}]", None ), + ({"method": "MaxDrawdown", "stopduration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " + "within 60 minutes.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From b36f333b2fed15bd864526b98a44a9604f96dc38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 06:52:19 +0100 Subject: [PATCH 128/281] Add new protections to full sample, documentation --- config_full.json.example | 14 +++++++++ docs/includes/protections.md | 59 ++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 839f99dbd..737015b41 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -85,6 +85,20 @@ { "method": "CooldownPeriod", "stopduration": 20 + }, + { + "method": "MaxDrawdown", + "lookback_period": 2000, + "trade_limit": 20, + "stop_duration": 10, + "max_allowed_drawdown": 0.2 + }, + { + "method": "LowProfitPairs", + "lookback_period": 360, + "trade_limit": 1, + "stop_duration": 2, + "required_profit": 0.02 } ], "exchange": { diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 716addb55..526c4d0a3 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -15,6 +15,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Available Protection Handlers * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. +* [`MaxDrawdown`](#maxdrawdown) Stop trading if max-drawdown is reached. * [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits * [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. @@ -23,33 +24,56 @@ All protection end times are rounded up to the next candle to avoid sudden, unex `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. +The below example stops trading for all pairs for 2 hours (120min) after the last trade if the bot hit stoploss 4 times within the last 24h. + ```json "protections": [ { "method": "StoplossGuard", - "lookback_period": 60, + "lookback_period": 1440, "trade_limit": 4, - "stop_duration": 60, + "stop_duration": 120, "only_per_pair": false } ], ``` !!! Note - `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. + `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the resulting profit was negative. `trade_limit` and `lookback_period` will need to be tuned for your strategy. +#### MaxDrawdown + +`MaxDrawdown` uses all trades within `lookback_period` (in minutes) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes) after the last trade - assuming that the bot needs some time to let markets recover. + +The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% considering all trades within the last 2 days (2880min). + +```json +"protections": [ + { + "method": "MaxDrawdown", + "lookback_period": 2880, + "trade_limit": 20, + "stop_duration": 720, + "max_allowed_drawdown": 0.2 + }, +], + +``` + #### Low Profit Pairs `LowProfitPairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). +The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 hours (360min). + ```json "protections": [ { "method": "LowProfitPairs", - "lookback_period": 60, - "trade_limit": 4, + "lookback_period": 360, + "trade_limit": 2, "stop_duration": 60, "required_profit": 0.02 } @@ -79,10 +103,11 @@ All protections are evaluated in the sequence they are defined. The below example: -* stops trading if more than 4 stoploss occur for all pairs within a 1 hour (60 minute) limit (`StoplossGuard`). * Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. -* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitPairs`) -* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 7 trades +* Stops trading if the last 2 days had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). +* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (1440min) limit (`StoplossGuard`). +* Locks all pairs that had 4 Trades within the last 6 hours (`60 * 6 = 360`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). +* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 4 trades. ```json "protections": [ @@ -90,23 +115,31 @@ The below example: "method": "CooldownPeriod", "stop_duration": 10 }, + { + "method": "MaxDrawdown", + "lookback_period": 2880, + "trade_limit": 20, + "stop_duration": 720, + "max_allowed_drawdown": 0.2 + }, { "method": "StoplossGuard", - "lookback_period": 60, + "lookback_period": 1440, "trade_limit": 4, - "stop_duration": 60 + "stop_duration": 120, + "only_per_pair": false }, { "method": "LowProfitPairs", "lookback_period": 360, - "trade_limit": 4, + "trade_limit": 2, "stop_duration": 60, "required_profit": 0.02 }, - { + { "method": "LowProfitPairs", "lookback_period": 1440, - "trade_limit": 7, + "trade_limit": 4, "stop_duration": 120, "required_profit": 0.01 } From f13e9ce5edb993b17e964d4ae3bae87baae97b68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 08:22:12 +0100 Subject: [PATCH 129/281] Improve docs --- docs/includes/protections.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 526c4d0a3..f5639565f 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -12,16 +12,21 @@ All protection end times are rounded up to the next candle to avoid sudden, unex !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). -### Available Protection Handlers +### Available Protections * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. * [`MaxDrawdown`](#maxdrawdown) Stop trading if max-drawdown is reached. * [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits * [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. +### Common settings to all Protections + +* `stop_duration` (minutes) - how long should protections be locked. +* `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). + #### Stoploss Guard -`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +`StoplossGuard` selects all trades within `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. The below example stops trading for all pairs for 2 hours (120min) after the last trade if the bot hit stoploss 4 times within the last 24h. @@ -63,7 +68,7 @@ The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% co #### Low Profit Pairs -`LowProfitPairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. +`LowProfitPairs` uses all trades for a pair within `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 hours (360min). @@ -93,8 +98,9 @@ The below example will stop trading a pair for 60 minutes if the pair does not h ], ``` -!!! Note: +!!! Note This Protection applies only at pair-level, and will never lock all pairs globally. + This Protection does not consider `lookback_period` as it only looks at the latest trade. ### Full example of Protections From eb952d77be2c9eaff24cab9d42a5111d28fefd13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 08:27:14 +0100 Subject: [PATCH 130/281] Move lookback_period to parent __init__ --- docs/includes/protections.md | 2 ++ freqtrade/plugins/protections/iprotection.py | 1 + freqtrade/plugins/protections/low_profit_pairs.py | 1 - freqtrade/plugins/protections/max_drawdown_protection.py | 1 - freqtrade/plugins/protections/stoploss_guard.py | 1 - 5 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index f5639565f..25d59a992 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,8 +21,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Common settings to all Protections +* `method` - Protection name to use. * `stop_duration` (minutes) - how long should protections be locked. * `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). +* `trade_limit` - How many trades are required at minimum (not used by all Protections). #### Stoploss Guard diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 2053ae741..60f83eea6 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -24,6 +24,7 @@ class IProtection(LoggingMixin, ABC): self._config = config self._protection_config = protection_config self._stop_duration = protection_config.get('stop_duration', 60) + self._lookback_period = protection_config.get('lookback_period', 60) LoggingMixin.__init__(self, logger) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 515f81521..70ef5b080 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -18,7 +18,6 @@ class LowProfitPairs(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 1) self._required_profit = protection_config.get('required_profit', 0.0) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e5625733c..2a83cdeba 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -21,7 +21,6 @@ class MaxDrawdown(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 1) self._max_allowed_drawdown = protection_config.get('max_allowed_drawdown', 0.0) # TODO: Implement checks to limit max_drawdown to sensible values diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index b6f430085..520607337 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -21,7 +21,6 @@ class StoplossGuard(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) self._disable_global_stop = protection_config.get('only_per_pair', False) From a93bb6853bbd6359deaa733adaa0491de5546fd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 10:21:03 +0100 Subject: [PATCH 131/281] Document *candles settings, implement validations --- docs/includes/protections.md | 12 ++++++++---- freqtrade/configuration/config_validation.py | 20 ++++++++++++++++++++ freqtrade/constants.py | 2 ++ tests/plugins/test_protections.py | 15 ++++++++------- tests/test_configuration.py | 18 ++++++++++++++++++ 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 25d59a992..2f704d83f 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,10 +21,14 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Common settings to all Protections -* `method` - Protection name to use. -* `stop_duration` (minutes) - how long should protections be locked. -* `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). -* `trade_limit` - How many trades are required at minimum (not used by all Protections). +| Parameter| Description | +|------------|-------------| +| method | Protection name to use.
**Datatype:** String, selected from [available Protections](#available-protections) +| stop_duration_candles | For how many candles should the lock be set?
**Datatype:** Positive integer (in candles) +| stop_duration | how many minutes should protections be locked.
Cannot be used together with `stop_duration_candles`.
**Datatype:** Float (in minutes) +| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections.
**Datatype:** Positive integer (in candles). +| lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
Cannot be used together with `lookback_period_candles`.
This setting may be ignored by some Protections.
**Datatype:** Float (in minutes) +| trade_limit | Number of trades required at minimum (not used by all Protections).
**Datatype:** Positive integer #### Stoploss Guard diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index ab21bc686..a6435d0e6 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -74,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: _validate_trailing_stoploss(conf) _validate_edge(conf) _validate_whitelist(conf) + _validate_protections(conf) _validate_unlimited_amount(conf) # validate configuration before returning @@ -155,3 +156,22 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: if (pl.get('method') == 'StaticPairList' and not conf.get('exchange', {}).get('pair_whitelist')): raise OperationalException("StaticPairList requires pair_whitelist to be set.") + + +def _validate_protections(conf: Dict[str, Any]) -> None: + """ + Validate protection configuration validity + """ + + for prot in conf.get('protections', []): + if ('stop_duration' in prot and 'stop_duration_candles' in prot): + raise OperationalException( + "Protections must specify either `stop_duration` or `stop_duration_candles`.\n" + f"Please fix the protection {prot.get('method')}" + ) + + if ('lookback_period' in prot and 'lookback_period_candle' in prot): + raise OperationalException( + "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" + f"Please fix the protection {prot.get('method')}" + ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dfc21b678..e7d7e80f6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -204,8 +204,10 @@ CONF_SCHEMA = { 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, 'stop_duration': {'type': 'number', 'minimum': 0.0}, + 'stop_duration_candles': {'type': 'number', 'minimum': 0}, 'trade_limit': {'type': 'number', 'minimum': 1}, 'lookback_period': {'type': 'number', 'minimum': 1}, + 'lookback_period_candles': {'type': 'number', 'minimum': 1}, }, 'required': ['method'], } diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index e5bbec431..29ff4e069 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -103,6 +103,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 1, + "stop_duration": 60, "only_per_pair": only_per_pair }] freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -158,7 +159,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair def test_CooldownPeriod(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "CooldownPeriod", - "stopduration": 60, + "stop_duration": 60, }] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" @@ -195,7 +196,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "LowProfitPairs", "lookback_period": 400, - "stopduration": 60, + "stop_duration": 60, "trade_limit": 2, "required_profit": 0.0, }] @@ -254,7 +255,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "MaxDrawdown", "lookback_period": 1000, - "stopduration": 60, + "stop_duration": 60, "trade_limit": 3, "max_allowed_drawdown": 0.15 }] @@ -315,21 +316,21 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ - ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, + ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " "2 stoplosses within 60 minutes.'}]", None ), - ({"method": "CooldownPeriod", "stopduration": 60}, + ({"method": "CooldownPeriod", "stop_duration": 60}, "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", None ), - ({"method": "LowProfitPairs", "stopduration": 60}, + ({"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " "profit < 0.0 within 60 minutes.'}]", None ), - ({"method": "MaxDrawdown", "stopduration": 60}, + ({"method": "MaxDrawdown", "lookback_period": 60, "stop_duration": 60}, "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " "within 60 minutes.'}]", None diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 167215f29..283f6a0f9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -879,6 +879,24 @@ def test_validate_whitelist(default_conf): validate_config_consistency(conf) +@pytest.mark.parametrize('protconf,expected', [ + ([], None), + ([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candle": 20, "stop_duration": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candle": 20, "lookback_period": 2000, + "stop_duration": 10}], r'Protections must specify either `lookback_period`.*'), + ([{"method": "StoplossGuard", "lookback_period": 20, "stop_duration": 10, + "stop_duration_candles": 10}], r'Protections must specify either `stop_duration`.*'), +]) +def test_validate_protections(default_conf, protconf, expected): + conf = deepcopy(default_conf) + conf['protections'] = protconf + if expected: + with pytest.raises(OperationalException, match=expected): + validate_config_consistency(conf) + else: + validate_config_consistency(conf) + def test_load_config_test_comments() -> None: """ From d4799e6aa3897db275920b48b615e7f6733c32ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 10:45:35 +0100 Subject: [PATCH 132/281] Implement *candle definitions --- docs/includes/protections.md | 57 +++++++++++--------- freqtrade/configuration/config_validation.py | 2 +- freqtrade/plugins/protections/iprotection.py | 12 ++++- tests/plugins/test_protections.py | 32 ++++++++++- tests/test_configuration.py | 5 +- 5 files changed, 76 insertions(+), 32 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 2f704d83f..210765176 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -30,20 +30,24 @@ All protection end times are rounded up to the next candle to avoid sudden, unex | lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
Cannot be used together with `lookback_period_candles`.
This setting may be ignored by some Protections.
**Datatype:** Float (in minutes) | trade_limit | Number of trades required at minimum (not used by all Protections).
**Datatype:** Positive integer +!!! Note "Durations" + Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles). + For more flexibility when testing different timeframes, all below examples will use the "candle" definition. + #### Stoploss Guard -`StoplossGuard` selects all trades within `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +`StoplossGuard` selects all trades within `lookback_period`, and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. -The below example stops trading for all pairs for 2 hours (120min) after the last trade if the bot hit stoploss 4 times within the last 24h. +The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. ```json "protections": [ { "method": "StoplossGuard", - "lookback_period": 1440, + "lookback_period_candles": 24, "trade_limit": 4, - "stop_duration": 120, + "stop_duration_candles": 4, "only_per_pair": false } ], @@ -57,15 +61,15 @@ The below example stops trading for all pairs for 2 hours (120min) after the las `MaxDrawdown` uses all trades within `lookback_period` (in minutes) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes) after the last trade - assuming that the bot needs some time to let markets recover. -The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% considering all trades within the last 2 days (2880min). +The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. ```json "protections": [ { "method": "MaxDrawdown", - "lookback_period": 2880, + "lookback_period_candles": 48, "trade_limit": 20, - "stop_duration": 720, + "stop_duration_candles": 12, "max_allowed_drawdown": 0.2 }, ], @@ -77,13 +81,13 @@ The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% co `LowProfitPairs` uses all trades for a pair within `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). -The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 hours (360min). +The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. ```json "protections": [ { "method": "LowProfitPairs", - "lookback_period": 360, + "lookback_period_candles": 6, "trade_limit": 2, "stop_duration": 60, "required_profit": 0.02 @@ -95,11 +99,13 @@ The below example will stop trading a pair for 60 minutes if the pair does not h `CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. +The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". + ```json "protections": [ { "method": "CooldownPeriod", - "stop_duration": 60 + "stop_duration_candle": 2 } ], ``` @@ -113,46 +119,47 @@ The below example will stop trading a pair for 60 minutes if the pair does not h All protections can be combined at will, also with different parameters, creating a increasing wall for under-performing pairs. All protections are evaluated in the sequence they are defined. -The below example: +The below example assumes a timeframe of 1 hour: -* Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. -* Stops trading if the last 2 days had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). -* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (1440min) limit (`StoplossGuard`). -* Locks all pairs that had 4 Trades within the last 6 hours (`60 * 6 = 360`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). -* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 4 trades. +* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled. +* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). +* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`). +* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). +* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades. ```json +"timeframe": "1h", "protections": [ { "method": "CooldownPeriod", - "stop_duration": 10 + "stop_duration_candles": 5 }, { "method": "MaxDrawdown", - "lookback_period": 2880, + "lookback_period_candles": 48, "trade_limit": 20, - "stop_duration": 720, + "stop_duration_candles": 4, "max_allowed_drawdown": 0.2 }, { "method": "StoplossGuard", - "lookback_period": 1440, + "lookback_period_candles": 24, "trade_limit": 4, - "stop_duration": 120, + "stop_duration_candles": 2, "only_per_pair": false }, { "method": "LowProfitPairs", - "lookback_period": 360, + "lookback_period_candles": 6, "trade_limit": 2, - "stop_duration": 60, + "stop_duration_candles": 60, "required_profit": 0.02 }, { "method": "LowProfitPairs", - "lookback_period": 1440, + "lookback_period_candles": 24, "trade_limit": 4, - "stop_duration": 120, + "stop_duration_candles": 2, "required_profit": 0.01 } ], diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index a6435d0e6..b8829b80f 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -170,7 +170,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None: f"Please fix the protection {prot.get('method')}" ) - if ('lookback_period' in prot and 'lookback_period_candle' in prot): + if ('lookback_period' in prot and 'lookback_period_candles' in prot): raise OperationalException( "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" f"Please fix the protection {prot.get('method')}" diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 60f83eea6..7a5a87f47 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple +from freqtrade.exchange import timeframe_to_minutes from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Trade @@ -23,8 +24,15 @@ class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config - self._stop_duration = protection_config.get('stop_duration', 60) - self._lookback_period = protection_config.get('lookback_period', 60) + tf_in_min = timeframe_to_minutes(config['timeframe']) + if 'stop_duration_candles' in protection_config: + self._stop_duration = (tf_in_min * protection_config.get('stop_duration_candles')) + else: + self._stop_duration = protection_config.get('stop_duration', 60) + if 'lookback_period_candles' in protection_config: + self._lookback_period = tf_in_min * protection_config.get('lookback_period_candles', 60) + else: + self._lookback_period = protection_config.get('lookback_period', 60) LoggingMixin.__init__(self, logger) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 29ff4e069..819ae805e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -3,9 +3,10 @@ from datetime import datetime, timedelta import pytest -from freqtrade.persistence import PairLocks, Trade -from freqtrade.strategy.interface import SellType from freqtrade import constants +from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.protectionmanager import ProtectionManager +from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -49,6 +50,33 @@ def test_protectionmanager(mocker, default_conf): assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) +@pytest.mark.parametrize('timeframe,expected,protconf', [ + ('1m', [20, 10], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}]), + ('5m', [100, 15], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 15}]), + ('1h', [1200, 40], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 40}]), + ('1d', [1440, 5], + [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration": 5}]), + ('1m', [20, 5], + [{"method": "StoplossGuard", "lookback_period": 20, "stop_duration_candles": 5}]), + ('5m', [15, 25], + [{"method": "StoplossGuard", "lookback_period": 15, "stop_duration_candles": 5}]), + ('1h', [50, 600], + [{"method": "StoplossGuard", "lookback_period": 50, "stop_duration_candles": 10}]), + ('1h', [60, 540], + [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration_candles": 9}]), +]) +def test_protections_init(mocker, default_conf, timeframe, expected, protconf): + default_conf['timeframe'] = timeframe + default_conf['protections'] = protconf + man = ProtectionManager(default_conf) + assert len(man._protection_handlers) == len(protconf) + assert man._protection_handlers[0]._lookback_period == expected[0] + assert man._protection_handlers[0]._stop_duration == expected[1] + + @pytest.mark.usefixtures("init_persistence") def test_stoploss_guard(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 283f6a0f9..bebbc1508 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -879,11 +879,12 @@ def test_validate_whitelist(default_conf): validate_config_consistency(conf) + @pytest.mark.parametrize('protconf,expected', [ ([], None), ([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None), - ([{"method": "StoplossGuard", "lookback_period_candle": 20, "stop_duration": 10}], None), - ([{"method": "StoplossGuard", "lookback_period_candle": 20, "lookback_period": 2000, + ([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candles": 20, "lookback_period": 2000, "stop_duration": 10}], r'Protections must specify either `lookback_period`.*'), ([{"method": "StoplossGuard", "lookback_period": 20, "stop_duration": 10, "stop_duration_candles": 10}], r'Protections must specify either `stop_duration`.*'), From c993831a04c8a92af179eb35a7bd6983458d0b56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 10:54:37 +0100 Subject: [PATCH 133/281] Add protections to startup messages --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/rpc_manager.py | 7 ++++++- tests/rpc/test_rpc_manager.py | 10 +++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ecc824a86..9fc342056 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -137,7 +137,7 @@ class FreqtradeBot(LoggingMixin): Called on startup and after reloading the bot - triggers notifications and performs startup tasks """ - self.rpc.startup_messages(self.config, self.pairlists) + self.rpc.startup_messages(self.config, self.pairlists, self.protections) if not self.edge: # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index b97a5357b..ab5e09ddd 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -62,7 +62,7 @@ class RPCManager: except NotImplementedError: logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") - def startup_messages(self, config: Dict[str, Any], pairlist) -> None: + def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, @@ -90,3 +90,8 @@ class RPCManager: 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) + if len(protections.name_list) > 0: + self.send_msg({ + 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'status': f'Using Protections {protections.short_desc()}' + }) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 4b715fc37..06706120f 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -137,7 +137,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) + rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) assert telegram_mock.call_count == 3 assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] @@ -147,10 +147,14 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: default_conf['whitelist'] = {'method': 'VolumePairList', 'config': {'number_assets': 20} } + default_conf['protections'] = [{"method": "StoplossGuard", + "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}] + freqtradebot = get_patched_freqtradebot(mocker, default_conf) - rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) - assert telegram_mock.call_count == 3 + rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) + assert telegram_mock.call_count == 4 assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] + assert 'StoplossGuard' in telegram_mock.call_args_list[-1][0][0]['status'] def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: From 0e2a43ab4dbd49e74f1aaca0da2d19dd41aa1dd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 11:08:54 +0100 Subject: [PATCH 134/281] Add duration_explanation functions --- .../plugins/protections/cooldown_period.py | 6 ++-- freqtrade/plugins/protections/iprotection.py | 33 +++++++++++++++++-- .../plugins/protections/low_profit_pairs.py | 6 ++-- .../protections/max_drawdown_protection.py | 8 ++--- .../plugins/protections/stoploss_guard.py | 2 +- tests/plugins/test_protections.py | 22 ++++++++++++- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 7b37b2303..e5eae01dd 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -22,13 +22,13 @@ class CooldownPeriod(IProtection): """ LockReason to use """ - return (f'Cooldown period for {self._stop_duration} min.') + return (f'Cooldown period for {self.stop_duration_str}.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return (f"{self.name} - Cooldown period of {self._stop_duration} min.") + return (f"{self.name} - Cooldown period of {self.stop_duration_str}.") def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: """ @@ -42,7 +42,7 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_once(f"Cooldown for {pair} for {self._stop_duration}.", logger.info) + self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) return True, until, self._reason() diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 7a5a87f47..684bf6cd3 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple from freqtrade.exchange import timeframe_to_minutes +from freqtrade.misc import plural from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Trade @@ -26,12 +27,16 @@ class IProtection(LoggingMixin, ABC): self._protection_config = protection_config tf_in_min = timeframe_to_minutes(config['timeframe']) if 'stop_duration_candles' in protection_config: - self._stop_duration = (tf_in_min * protection_config.get('stop_duration_candles')) + self._stop_duration_candles = protection_config.get('stop_duration_candles', 1) + self._stop_duration = (tf_in_min * self._stop_duration_candles) else: + self._stop_duration_candles = None self._stop_duration = protection_config.get('stop_duration', 60) if 'lookback_period_candles' in protection_config: - self._lookback_period = tf_in_min * protection_config.get('lookback_period_candles', 60) + self._lookback_period_candles = protection_config.get('lookback_period_candles', 1) + self._lookback_period = tf_in_min * self._lookback_period_candles else: + self._lookback_period_candles = None self._lookback_period = protection_config.get('lookback_period', 60) LoggingMixin.__init__(self, logger) @@ -40,6 +45,30 @@ class IProtection(LoggingMixin, ABC): def name(self) -> str: return self.__class__.__name__ + @property + def stop_duration_str(self) -> str: + """ + Output configured stop duration in either candles or minutes + """ + if self._stop_duration_candles: + return (f"{self._stop_duration_candles} " + f"{plural(self._stop_duration_candles, 'candle', 'candles')}") + else: + return (f"{self._stop_duration} " + f"{plural(self._stop_duration, 'minute', 'minutes')}") + + @property + def lookback_period_str(self) -> str: + """ + Output configured lookback period in either candles or minutes + """ + if self._lookback_period_candles: + return (f"{self._lookback_period_candles} " + f"{plural(self._lookback_period_candles, 'candle', 'candles')}") + else: + return (f"{self._lookback_period} " + f"{plural(self._lookback_period, 'minute', 'minutes')}") + @abstractmethod def short_desc(self) -> str: """ diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 70ef5b080..4721ea1a2 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -26,14 +26,14 @@ class LowProfitPairs(IProtection): Short method description - used for startup-messages """ return (f"{self.name} - Low Profit Protection, locks pairs with " - f"profit < {self._required_profit} within {self._lookback_period} minutes.") + f"profit < {self._required_profit} within {self.lookback_period_str}.") def _reason(self, profit: float) -> str: """ LockReason to use """ - return (f'{profit} < {self._required_profit} in {self._lookback_period} min, ' - f'locking for {self._stop_duration} min.') + return (f'{profit} < {self._required_profit} in {self.lookback_period_str}, ' + f'locking for {self.stop_duration_str}.') def _low_profit(self, date_now: datetime, pair: str) -> ProtectionReturn: """ diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 2a83cdeba..e0c91243b 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -30,14 +30,14 @@ class MaxDrawdown(IProtection): Short method description - used for startup-messages """ return (f"{self.name} - Max drawdown protection, stop trading if drawdown is > " - f"{self._max_allowed_drawdown} within {self._lookback_period} minutes.") + f"{self._max_allowed_drawdown} within {self.lookback_period_str}.") def _reason(self, drawdown: float) -> str: """ LockReason to use """ - return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' - f'locking for {self._stop_duration} min.') + return (f'{drawdown} > {self._max_allowed_drawdown} in {self.lookback_period_str}, ' + f'locking for {self.stop_duration_str}.') def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: """ @@ -62,7 +62,7 @@ class MaxDrawdown(IProtection): if drawdown > self._max_allowed_drawdown: self.log_once( f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" - f" within {self._lookback_period} minutes.", logger.info) + f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(drawdown) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 520607337..7a13ead57 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -29,7 +29,7 @@ class StoplossGuard(IProtection): Short method description - used for startup-messages """ return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " - f"within {self._lookback_period} minutes.") + f"within {self.lookback_period_str}.") def _reason(self) -> str: """ diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 819ae805e..22fe33e19 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -350,7 +350,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): None ), ({"method": "CooldownPeriod", "stop_duration": 60}, - "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 minutes.'}]", None ), ({"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, @@ -363,6 +363,26 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): "within 60 minutes.'}]", None ), + ({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2, + "stop_duration": 60}, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses within 12 candles.'}]", + None + ), + ({"method": "CooldownPeriod", "stop_duration_candles": 5}, + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 5 candles.'}]", + None + ), + ({"method": "LowProfitPairs", "lookback_period_candles": 11, "stop_duration": 60}, + "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " + "profit < 0.0 within 11 candles.'}]", + None + ), + ({"method": "MaxDrawdown", "lookback_period_candles": 20, "stop_duration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " + "within 20 candles.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From 64d6c7bb651765ff5ab1071879f5197e7fbba025 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 11:17:11 +0100 Subject: [PATCH 135/281] Update developer docs --- docs/developer.md | 6 ++++-- freqtrade/plugins/protectionmanager.py | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 05b518184..f1d658ab8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -202,10 +202,10 @@ For that reason, they must implement the following methods: The `until` portion should be calculated using the provided `calculate_lock_end()` method. -All Protections should use `"stop_duration"` to define how long a a pair (or all pairs) should be locked. +All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a a pair (or all pairs) should be locked. The content of this is made available as `self._stop_duration` to the each Protection. -If your protection requires a look-back period, please use `"lookback_period"` to keep different protections aligned. +If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned. #### Global vs. local stops @@ -230,6 +230,8 @@ The method `global_stop()` will be called whenever a trade closed (sell order co Protections should calculate the lock end time based on the last trade it considers. This avoids relocking should the lookback-period be longer than the actual lock period. +The `IProtection` parent class provides a helper method for this in `calculate_lock_end()`. + --- ## Implement a new Exchange (WIP) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index d12f4ba80..03a09cc58 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -20,9 +20,6 @@ class ProtectionManager(): self._protection_handlers: List[IProtection] = [] for protection_handler_config in self._config.get('protections', []): - if 'method' not in protection_handler_config: - logger.warning(f"No method found in {protection_handler_config}, ignoring.") - continue protection_handler = ProtectionResolver.load_protection( protection_handler_config['method'], config=config, From 3426e99b8b3ad3f93eac53f474c84fc20c461c8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 11:37:57 +0100 Subject: [PATCH 136/281] Improve formatting of protection startup message --- freqtrade/rpc/rpc_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index ab5e09ddd..c42878f99 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -91,7 +91,8 @@ class RPCManager: f'based on {pairlist.short_desc()}' }) if len(protections.name_list) > 0: + prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()]) self.send_msg({ 'type': RPCMessageType.STARTUP_NOTIFICATION, - 'status': f'Using Protections {protections.short_desc()}' + 'status': f'Using Protections: \n{prots}' }) From 98c88fa58e890630a2c9221d48a31c42423a28ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Nov 2020 20:09:34 +0100 Subject: [PATCH 137/281] Prepare protections for backtesting --- freqtrade/persistence/models.py | 41 +++++++++++++++++++ freqtrade/plugins/protectionmanager.py | 12 +++--- .../plugins/protections/cooldown_period.py | 17 ++++---- .../plugins/protections/low_profit_pairs.py | 16 ++++---- .../protections/max_drawdown_protection.py | 7 +--- .../plugins/protections/stoploss_guard.py | 25 ++++++----- 6 files changed, 84 insertions(+), 34 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 04d5a7695..d262a6784 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -202,6 +202,10 @@ class Trade(_DECL_BASE): """ __tablename__ = 'trades' + use_db: bool = True + # Trades container for backtesting + trades: List['Trade'] = [] + id = Column(Integer, primary_key=True) orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") @@ -562,6 +566,43 @@ class Trade(_DECL_BASE): else: return Trade.query + @staticmethod + def get_trades_proxy(*, pair: str = None, is_open: bool = None, + open_date: datetime = None, close_date: datetime = None, + ) -> List['Trade']: + """ + Helper function to query Trades. + Returns a List of trades, filtered on the parameters given. + In live mode, converts the filter to a database query and returns all rows + In Backtest mode, uses filters on Trade.trades to get the result. + + :return: unsorted List[Trade] + """ + if Trade.use_db: + trade_filter = [] + if pair: + trade_filter.append(Trade.pair == pair) + if open_date: + trade_filter.append(Trade.open_date > open_date) + if close_date: + trade_filter.append(Trade.close_date > close_date) + if is_open is not None: + trade_filter.append(Trade.is_open.is_(is_open)) + return Trade.get_trades(trade_filter).all() + else: + # Offline mode - without database + sel_trades = [trade for trade in Trade.trades] + if pair: + sel_trades = [trade for trade in sel_trades if trade.pair == pair] + if open_date: + sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] + if close_date: + sel_trades = [trade for trade in sel_trades if trade.close_date + and trade.close_date > close_date] + if is_open is not None: + sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades + @staticmethod def get_open_trades() -> List[Any]: """ diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 03a09cc58..a8edd4e4b 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -3,7 +3,7 @@ Protection manager class """ import logging from datetime import datetime, timezone -from typing import Dict, List +from typing import Dict, List, Optional from freqtrade.persistence import PairLocks from freqtrade.plugins.protections import IProtection @@ -43,8 +43,9 @@ class ProtectionManager(): """ return [{p.name: p.short_desc()} for p in self._protection_handlers] - def global_stop(self) -> bool: - now = datetime.now(timezone.utc) + def global_stop(self, now: Optional[datetime] = None) -> bool: + if not now: + now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: if protection_handler.has_global_stop: @@ -57,8 +58,9 @@ class ProtectionManager(): result = True return result - def stop_per_pair(self, pair) -> bool: - now = datetime.now(timezone.utc) + def stop_per_pair(self, pair, now: Optional[datetime] = None) -> bool: + if not now: + now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: if protection_handler.has_local_stop: diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index e5eae01dd..2d7d7b4c7 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -35,13 +35,16 @@ class CooldownPeriod(IProtection): Get last trade for this pair """ look_back_until = date_now - timedelta(minutes=self._stop_duration) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - Trade.pair == pair, - ] - trade = Trade.get_trades(filters).first() - if trade: + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # Trade.pair == pair, + # ] + # trade = Trade.get_trades(filters).first() + trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) + if trades: + # Get latest trade + trade = sorted(trades, key=lambda t: t.close_date)[-1] self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 4721ea1a2..9d5ed35b4 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -40,13 +40,15 @@ class LowProfitPairs(IProtection): Evaluate recent trades for pair """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - ] - if pair: - filters.append(Trade.pair == pair) - trades = Trade.get_trades(filters).all() + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # ] + # if pair: + # filters.append(Trade.pair == pair) + + trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) + # trades = Trade.get_trades(filters).all() if len(trades) < self._trade_limit: # Not enough trades in the relevant period return False, None, None diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e0c91243b..f1c77d1d9 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -44,11 +44,8 @@ class MaxDrawdown(IProtection): Evaluate recent trades for drawdown ... """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - ] - trades = Trade.get_trades(filters).all() + + trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) trades_df = pd.DataFrame([trade.to_json() for trade in trades]) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 7a13ead57..4dbc71048 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -43,16 +43,21 @@ class StoplossGuard(IProtection): Evaluate recent trades """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - or_(Trade.sell_reason == SellType.STOP_LOSS.value, - and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, - Trade.close_profit < 0)) - ] - if pair: - filters.append(Trade.pair == pair) - trades = Trade.get_trades(filters).all() + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # or_(Trade.sell_reason == SellType.STOP_LOSS.value, + # and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, + # Trade.close_profit < 0)) + # ] + # if pair: + # filters.append(Trade.pair == pair) + # trades = Trade.get_trades(filters).all() + + trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) + trades = [trade for trade in trades1 if trade.sell_reason == SellType.STOP_LOSS + or (trade.sell_reason == SellType.TRAILING_STOP_LOSS + and trade.close_profit < 0)] if len(trades) > self._trade_limit: self.log_once(f"Trading stopped due to {self._trade_limit} " From b606936eb70131856a347d54540a784e23035085 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Nov 2020 20:17:47 +0100 Subject: [PATCH 138/281] Make changes to backtesting to incorporate protections --- freqtrade/optimize/backtesting.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 883f7338c..1d183152c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,7 +21,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType @@ -115,6 +116,11 @@ class Backtesting: else: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) + Trade.use_db = False + PairLocks.timeframe = self.config['timeframe'] + PairLocks.use_db = False + self.protections = ProtectionManager(self.config) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy @@ -235,6 +241,10 @@ class Backtesting: trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + trade.close_date = sell_row[DATE_IDX] + trade.sell_reason = sell.sell_type + trade.close(closerate) + return BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio(rate=closerate), profit_abs=trade.calc_profit(rate=closerate), @@ -261,6 +271,7 @@ class Backtesting: if len(open_trades[pair]) > 0: for trade in open_trades[pair]: sell_row = data[pair][-1] + trade_entry = BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio( rate=sell_row[OPEN_IDX]), @@ -320,6 +331,8 @@ class Backtesting: while tmp <= end_date: open_trade_count_start = open_trade_count + self.protections.global_stop(tmp) + for i, pair in enumerate(data): if pair not in indexes: indexes[pair] = 0 @@ -342,7 +355,8 @@ class Backtesting: 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 row[BUY_IDX] == 1 and row[SELL_IDX] != 1 + and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): # Enter trade trade = Trade( pair=pair, @@ -361,6 +375,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") open_trades[pair].append(trade) + Trade.trades.append(trade) for trade in open_trades[pair]: # since indexes has been incremented before, we need to go one step back to @@ -372,6 +387,7 @@ class Backtesting: open_trade_count -= 1 open_trades[pair].remove(trade) trades.append(trade_entry) + self.protections.stop_per_pair(pair, row[DATE_IDX]) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) From 9f34aebdaa4ebdbeee78130d03e35f55352551e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Nov 2020 20:21:32 +0100 Subject: [PATCH 139/281] Allow closing trades without message --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/models.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1d183152c..f80976a20 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -243,7 +243,7 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type - trade.close(closerate) + trade.close(closerate, show_msg=False) return BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio(rate=closerate), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d262a6784..9b8f561b8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -411,7 +411,7 @@ class Trade(_DECL_BASE): raise ValueError(f'Unknown order type: {order_type}') cleanup_db() - def close(self, rate: float) -> None: + def close(self, rate: float, *, show_msg: bool = False) -> None: """ Sets close_rate to the given rate, calculates total profit and marks trade as closed @@ -423,10 +423,11 @@ class Trade(_DECL_BASE): self.is_open = False self.sell_order_status = 'closed' self.open_order_id = None - logger.info( - 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', - self - ) + if show_msg: + logger.info( + 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', + self + ) def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float], side: str) -> None: From 32189d27c82db0a3239075c6259a1770cb78e5c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 20:05:56 +0100 Subject: [PATCH 140/281] Disable output from plugins in backtesting --- freqtrade/optimize/backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f80976a20..7ead5ca24 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -25,6 +25,7 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType +from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) @@ -68,6 +69,8 @@ class Backtesting: """ def __init__(self, config: Dict[str, Any]) -> None: + + LoggingMixin.show_output = False self.config = config # Reset keys for backtesting From e2d15f40824cdc26dc55081eda1e5fa0493cbc06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Nov 2020 20:29:29 +0100 Subject: [PATCH 141/281] Add parameter to enable protections for backtesting --- freqtrade/commands/arguments.py | 6 ++++-- freqtrade/commands/cli_options.py | 8 ++++++++ freqtrade/configuration/configuration.py | 3 +++ freqtrade/optimize/backtesting.py | 11 ++++++++--- freqtrade/optimize/hyperopt.py | 2 ++ 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index aa58ff585..a7ae969f4 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -20,11 +20,13 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", + "enable_protections", "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", - "position_stacking", "epochs", "spaces", - "use_max_market_positions", "print_all", + "position_stacking", "use_max_market_positions", + "enable_protections", + "epochs", "spaces", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_loss"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 619a300ae..668b4abf5 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -144,6 +144,14 @@ AVAILABLE_CLI_OPTIONS = { action='store_false', default=True, ), + "enable_protections": Arg( + '--enable-protections', '--enableprotections', + help='Enable protections for backtesting.' + 'Will slow backtesting down by a considerable amount, but will include ' + 'configured protections', + action='store_true', + default=False, + ), "strategy_list": Arg( '--strategy-list', help='Provide a space-separated list of strategies to backtest. ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 1ca3187fb..7bf3e6bf2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -211,6 +211,9 @@ class Configuration: self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') + self._args_to_config( + config, argname='enable_protections', + logstring='Parameter --enable-protections detected, enabling Protections. ...') # Setting max_open_trades to infinite if -1 if config.get('max_open_trades') == -1: config['max_open_trades'] = float('inf') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7ead5ca24..56cc426ac 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -297,7 +297,8 @@ class Backtesting: def backtest(self, processed: Dict, stake_amount: float, start_date: datetime, end_date: datetime, - max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: + max_open_trades: int = 0, position_stacking: bool = False, + enable_protections: bool = False) -> DataFrame: """ Implement backtesting functionality @@ -311,6 +312,7 @@ class Backtesting: :param end_date: backtesting timerange end datetime :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited :param position_stacking: do we allow position stacking? + :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ logger.debug(f"Run backtest, stake_amount: {stake_amount}, " @@ -334,7 +336,8 @@ class Backtesting: while tmp <= end_date: open_trade_count_start = open_trade_count - self.protections.global_stop(tmp) + if enable_protections: + self.protections.global_stop(tmp) for i, pair in enumerate(data): if pair not in indexes: @@ -390,7 +393,8 @@ class Backtesting: open_trade_count -= 1 open_trades[pair].remove(trade) trades.append(trade_entry) - self.protections.stop_per_pair(pair, row[DATE_IDX]) + if enable_protections: + self.protections.stop_per_pair(pair, row[DATE_IDX]) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) @@ -446,6 +450,7 @@ class Backtesting: end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=position_stacking, + enable_protections=self.config.get('enable_protections'), ) all_results[self.strategy.get_strategy_name()] = { 'results': results, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7870ba1cf..2a2f5b472 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -542,6 +542,8 @@ class Hyperopt: end_date=max_date.datetime, 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) From 946fb094553ab32854d6992f9ecc58e6db26ea42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 06:51:54 +0100 Subject: [PATCH 142/281] Update help command output --- docs/bot-usage.md | 43 +++++++++++++++++++++++++----------- docs/includes/protections.md | 3 +++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4d07435c7..5820b3cc7 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -213,9 +213,11 @@ Backtesting also uses the config specified via `-c/--config`. usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] - [--timerange TIMERANGE] [--max-open-trades INT] + [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] + [--eps] [--dmmp] [--enable-protections] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -226,6 +228,9 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -241,6 +246,10 @@ optional arguments: Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to backtest. Please note that ticker-interval needs to be @@ -296,13 +305,14 @@ to find optimal parameter values for your strategy. usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [-e INT] + [--dmmp] [--enable-protections] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] - [--dmmp] [--print-all] [--no-color] [--print-json] - [-j JOBS] [--random-state INT] [--min-trades INT] + [--print-all] [--no-color] [--print-json] [-j JOBS] + [--random-state INT] [--min-trades INT] [--hyperopt-loss NAME] optional arguments: @@ -312,6 +322,9 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -327,14 +340,18 @@ optional arguments: --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). - -e INT, --epochs INT Specify number of epochs (default: 100). - --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] - Specify which parameters to hyperopt. Space-separated - list. --dmmp, --disable-max-market-positions Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections + -e INT, --epochs INT Specify number of epochs (default: 100). + --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] + Specify which parameters to hyperopt. Space-separated + list. --print-all Print all results, not only the best ones. --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. @@ -353,10 +370,10 @@ optional arguments: class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in - Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, - OnlyProfitHyperOptLoss, SharpeHyperOptLoss, - SharpeHyperOptLossDaily, SortinoHyperOptLoss, - SortinoHyperOptLossDaily. + Hyperopt-loss-functions are: + ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, + SharpeHyperOptLoss, SharpeHyperOptLossDaily, + SortinoHyperOptLoss, SortinoHyperOptLossDaily Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 210765176..351cfcac3 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -12,6 +12,9 @@ All protection end times are rounded up to the next candle to avoid sudden, unex !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). +!!! Note "Backtesting" + Protections are supported by backtesting and hyperopt, but must be enabled by using the `--enable-protections` flag. + ### Available Protections * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. From a3f9cd2c26cfaf70033f99d4d4a1e8cffc5f9c54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 07:38:09 +0100 Subject: [PATCH 143/281] Only load protections when necessary --- freqtrade/optimize/backtesting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 56cc426ac..1819e5617 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -122,7 +122,8 @@ class Backtesting: Trade.use_db = False PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False - self.protections = ProtectionManager(self.config) + if self.config.get('enable_protections', False): + self.protections = ProtectionManager(self.config) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -450,7 +451,7 @@ class Backtesting: end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=position_stacking, - enable_protections=self.config.get('enable_protections'), + enable_protections=self.config.get('enable_protections', False), ) all_results[self.strategy.get_strategy_name()] = { 'results': results, From 75a5161650072fc5592bcb63e6f11cc8e2aab07f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 09:53:13 +0100 Subject: [PATCH 144/281] Support multis-strategy backtests with protections --- freqtrade/optimize/backtesting.py | 14 ++++++++ freqtrade/persistence/models.py | 8 +++++ freqtrade/persistence/pairlock_middleware.py | 8 +++++ .../plugins/protections/stoploss_guard.py | 4 +-- tests/optimize/test_backtesting.py | 34 +++++++++++++++++-- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1819e5617..e3f5e7671 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -120,8 +120,10 @@ class Backtesting: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) Trade.use_db = False + Trade.reset_trades() PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False + PairLocks.reset_locks() if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) @@ -130,6 +132,11 @@ class Backtesting: # Load one (first) strategy self._set_strategy(self.strategylist[0]) + def __del__(self): + LoggingMixin.show_output = True + PairLocks.use_db = True + Trade.use_db = True + def _set_strategy(self, strategy): """ Load strategy into backtesting @@ -321,6 +328,13 @@ class Backtesting: f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) trades = [] + PairLocks.use_db = False + Trade.use_db = False + if enable_protections: + # Reset persisted data - used for protections only + + PairLocks.reset_locks() + Trade.reset_trades() # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9b8f561b8..07f4b5a4f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -327,6 +327,14 @@ class Trade(_DECL_BASE): 'open_order_id': self.open_order_id, } + @staticmethod + def reset_trades() -> None: + """ + Resets all trades. Only active for backtesting mode. + """ + if not Trade.use_db: + Trade.trades = [] + def adjust_min_max_rates(self, current_price: float) -> None: """ Adjust the max_rate and min_rate. diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 6ce91ee6b..8644146d8 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -21,6 +21,14 @@ class PairLocks(): timeframe: str = '' + @staticmethod + def reset_locks() -> None: + """ + Resets all locks. Only active for backtesting mode. + """ + if not PairLocks.use_db: + PairLocks.locks = [] + @staticmethod def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None: """ diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 4dbc71048..71e74880c 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -55,8 +55,8 @@ class StoplossGuard(IProtection): # trades = Trade.get_trades(filters).all() trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) - trades = [trade for trade in trades1 if trade.sell_reason == SellType.STOP_LOSS - or (trade.sell_reason == SellType.TRAILING_STOP_LOSS + trades = [trade for trade in trades1 if str(trade.sell_reason) == SellType.STOP_LOSS.value + or (str(trade.sell_reason) == SellType.TRAILING_STOP_LOSS.value and trade.close_profit < 0)] if len(trades) > self._trade_limit: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 45cbea68e..15ad18bf9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -95,6 +95,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: end_date=max_date, max_open_trades=1, position_stacking=False, + enable_protections=config.get('enable_protections', False), ) # results :: assert len(results) == num_results @@ -532,10 +533,39 @@ def test_processed(default_conf, mocker, testdatadir) -> None: def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir) -> None: - # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 19], ['lower', 0], ['sine', 35]] + tests = [ + ['sine', 35], + ['raise', 19], + ['lower', 0], + ['sine', 35], + ['raise', 19] + ] + # While buy-signals are unrealistic, running backtesting + # over and over again should not cause different results + for [contour, numres] in tests: + simple_backtest(default_conf, contour, numres, mocker, testdatadir) + +def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: + # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic + default_conf['protections'] = [ + { + "method": "CooldownPeriod", + "stop_duration": 3, + }] + + default_conf['enable_protections'] = True + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + tests = [ + ['sine', 9], + ['raise', 10], + ['lower', 0], + ['sine', 9], + ['raise', 10], + ] + # While buy-signals are unrealistic, running backtesting + # over and over again should not cause different results for [contour, numres] in tests: simple_backtest(default_conf, contour, numres, mocker, testdatadir) From bb51da82978efe7592ed7a14619ab74a91eef4df Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:38:15 +0100 Subject: [PATCH 145/281] Fix slow backtest due to protections --- freqtrade/optimize/backtesting.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e3f5e7671..2684a249c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -173,6 +173,17 @@ class Backtesting: return data, timerange + def prepare_backtest(self, enable_protections): + """ + Backtesting setup method - called once for every call to "backtest()". + """ + PairLocks.use_db = False + Trade.use_db = False + if enable_protections: + # Reset persisted data - used for protections only + PairLocks.reset_locks() + Trade.reset_trades() + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -328,13 +339,7 @@ class Backtesting: f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) trades = [] - PairLocks.use_db = False - Trade.use_db = False - if enable_protections: - # Reset persisted data - used for protections only - - PairLocks.reset_locks() - Trade.reset_trades() + self.prepare_backtest(enable_protections) # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) @@ -351,9 +356,6 @@ class Backtesting: while tmp <= end_date: open_trade_count_start = open_trade_count - if enable_protections: - self.protections.global_stop(tmp) - for i, pair in enumerate(data): if pair not in indexes: indexes[pair] = 0 @@ -410,6 +412,7 @@ class Backtesting: trades.append(trade_entry) if enable_protections: self.protections.stop_per_pair(pair, row[DATE_IDX]) + self.protections.global_stop(tmp) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) From 57a4044eb0a74209c206941ff9ceb3b763bcf713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Nov 2020 11:37:10 +0100 Subject: [PATCH 146/281] Enhance test verifying that locks are not replaced --- tests/plugins/test_protections.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 22fe33e19..e997c5526 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest @@ -123,6 +123,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert log_has_re(message, caplog) assert PairLocks.is_global_lock() + # Test 5m after lock-period - this should try and relock the pair, but end-time + # should be the previous end-time + end_time = PairLocks.get_pair_longest_lock('*').lock_end_time + timedelta(minutes=5) + assert freqtrade.protections.global_stop(end_time) + assert not PairLocks.is_global_lock(end_time) + @pytest.mark.parametrize('only_per_pair', [False, True]) @pytest.mark.usefixtures("init_persistence") From 5849d07497f1d36bfa0380c18f241ba6feabc8e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 06:51:59 +0100 Subject: [PATCH 147/281] Export locks as part of backtesting --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/optimize_reports.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2684a249c..5bb7eaf74 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -473,6 +473,7 @@ class Backtesting: all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, + 'locks': PairLocks.locks, } stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b3799856e..d029ecd13 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -266,6 +266,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 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, From effc96e92b352f819571a74aab5bcd8db1669803 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Dec 2020 07:42:39 +0100 Subject: [PATCH 148/281] Improve tests for backtest protections --- docs/developer.md | 5 +- docs/includes/protections.md | 4 +- freqtrade/mixins/logging_mixin.py | 2 - freqtrade/plugins/__init__.py | 2 - .../plugins/protections/stoploss_guard.py | 2 - tests/optimize/test_backtesting.py | 48 +++++++++++-------- tests/optimize/test_optimize_reports.py | 3 +- tests/plugins/test_protections.py | 2 +- 8 files changed, 37 insertions(+), 31 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index f1d658ab8..48b021027 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -121,8 +121,8 @@ The base-class provides an instance of the exchange (`self._exchange`) the pairl self._pairlist_pos = pairlist_pos ``` -!!! Note - You'll need to register your pairlist in `constants.py` under the variable `AVAILABLE_PAIRLISTS` - otherwise it will not be selectable. +!!! Tip + Don't forget to register your pairlist in `constants.py` under the variable `AVAILABLE_PAIRLISTS` - otherwise it will not be selectable. Now, let's step through the methods which require actions: @@ -184,6 +184,7 @@ No protection should use datetime directly, but use the provided `date_now` vari !!! Tip "Writing a new Protection" Best copy one of the existing Protections to have a good example. + Don't forget to register your protection in `constants.py` under the variable `AVAILABLE_PROTECTIONS` - otherwise it will not be selectable. #### Implementation of a new protection diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 351cfcac3..a8caf55b1 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,7 +1,7 @@ ## Protections !!! Warning "Beta feature" - This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Issue. + This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Github Issue. Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. @@ -13,7 +13,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). !!! Note "Backtesting" - Protections are supported by backtesting and hyperopt, but must be enabled by using the `--enable-protections` flag. + Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag. ### Available Protections diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index e9921e1ec..2e1c20a52 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -1,5 +1,3 @@ - - from typing import Callable from cachetools import TTLCache, cached diff --git a/freqtrade/plugins/__init__.py b/freqtrade/plugins/__init__.py index 96943268b..e69de29bb 100644 --- a/freqtrade/plugins/__init__.py +++ b/freqtrade/plugins/__init__.py @@ -1,2 +0,0 @@ -# flake8: noqa: F401 -# from freqtrade.plugins.protectionmanager import ProtectionManager diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 71e74880c..193907ddc 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -3,8 +3,6 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict -from sqlalchemy import and_, or_ - from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.strategy.interface import SellType diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 15ad18bf9..547e55db8 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -79,7 +79,7 @@ def load_data_test(what, testdatadir): fill_missing=True)} -def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: +def simple_backtest(config, contour, mocker, testdatadir) -> None: patch_exchange(mocker) config['timeframe'] = '1m' backtesting = Backtesting(config) @@ -98,7 +98,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: enable_protections=config.get('enable_protections', False), ) # results :: - assert len(results) == num_results + return results # FIX: fixturize this? @@ -532,23 +532,9 @@ def test_processed(default_conf, mocker, testdatadir) -> None: assert col in cols -def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [ - ['sine', 35], - ['raise', 19], - ['lower', 0], - ['sine', 35], - ['raise', 19] - ] - # While buy-signals are unrealistic, running backtesting - # over and over again should not cause different results - for [contour, numres] in tests: - simple_backtest(default_conf, contour, numres, mocker, testdatadir) - - def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: - # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic + # While this test IS a copy of test_backtest_pricecontours, it's needed to ensure + # results do not carry-over to the next run, which is not given by using parametrize. default_conf['protections'] = [ { "method": "CooldownPeriod", @@ -567,7 +553,31 @@ 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: - simple_backtest(default_conf, contour, numres, mocker, testdatadir) + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres + + +@pytest.mark.parametrize('protections,contour,expected', [ + (None, 'sine', 35), + (None, 'raise', 19), + (None, 'lower', 0), + (None, 'sine', 35), + (None, 'raise', 19), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'lower', 0), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10), +]) +def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, + protections, contour, expected) -> None: + if protections: + default_conf['protections'] = protections + default_conf['enable_protections'] = True + + 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 def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index d04929164..a0e1932ff 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -76,7 +76,8 @@ def test_generate_backtest_stats(default_conf, testdatadir): "sell_reason": [SellType.ROI, SellType.STOP_LOSS, SellType.ROI, SellType.FORCE_SELL] }), - 'config': default_conf} + 'config': default_conf, + 'locks': []} } timerange = TimeRange.parse_timerange('1510688220-1510700340') min_date = Arrow.fromtimestamp(1510688220) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index e997c5526..2ad03a97c 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta import pytest From e873cafdc49d46c2398550a77bd29dd61816a050 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 14:54:39 +0100 Subject: [PATCH 149/281] Beautify code a bit --- freqtrade/rpc/rpc.py | 20 ++++++++++---------- freqtrade/rpc/telegram.py | 17 +++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c4b4117ff..49e5bc2d2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -275,38 +275,38 @@ class RPC: "trades_count": len(output) } - def _rpc_stats(self): + def _rpc_stats(self) -> Dict[str, Any]: """ Generate generic stats for trades in database """ def trade_win_loss(trade): if trade.close_profit > 0: - return 'Wins' + return 'wins' elif trade.close_profit < 0: - return 'Losses' + return 'losses' else: - return 'Draws' + return 'draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) # Sell reason sell_reasons = {} for trade in trades: if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0} sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 # Duration - dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} + dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' - draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' - losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A' + draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A' + losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} - return sell_reasons, durations + return {'sell_reasons': sell_reasons, 'durations': durations} def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 76d9292b4..25965e05f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,9 +3,9 @@ """ This module manage Telegram communication """ -from datetime import timedelta import json import logging +from datetime import timedelta from typing import Any, Callable, Dict, List, Union import arrow @@ -395,9 +395,8 @@ class Telegram(RPC): """ Handler for /stats Show stats of recent trades - :return: None """ - sell_reasons, durations = self._rpc_stats() + stats = self._rpc_stats() sell_reasons_tabulate = [] reason_map = { @@ -409,26 +408,24 @@ class Telegram(RPC): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - for reason, count in sell_reasons.items(): + for reason, count in stats['sell_reasons'].items(): sell_reasons_tabulate.append([ reason_map.get(reason, reason), sum(count.values()), - count['Wins'], - # count['Draws'], - count['Losses'] + count['wins'], + count['losses'] ]) sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) - + durations = stats['durations'] duration_msg = tabulate([ ['Wins', str(timedelta(seconds=durations['wins'])) if durations['wins'] != 'N/A' else 'N/A'], - # ['Draws', str(timedelta(seconds=durations['draws']))], ['Losses', str(timedelta(seconds=durations['losses'])) if durations['losses'] != 'N/A' else 'N/A'] - ], + ], headers=['', 'Avg. Duration'] ) msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") From 81410fb4044c0d6238441ce86dcaed95d3d3e975 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:03:16 +0100 Subject: [PATCH 150/281] Document /stats for telegram --- docs/telegram-usage.md | 1 + freqtrade/rpc/telegram.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f4bd0a12a..c940f59ac 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -113,6 +113,7 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells | `/whitelist` | Show the current whitelist | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/edge` | Show validated pairs by Edge if it is enabled. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 25965e05f..b6c0a1f3f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -787,6 +787,8 @@ class Telegram(RPC): "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" + "*/stats:* `Shows Wins / losses by Sell reason as well as " + "Avg. holding durationsfor buys and sells.`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/locks:* `Show currently locked pairs`\n" "*/balance:* `Show account balance per currency`\n" From 3ab5514697c294a9ab5918dc44e408f2f88bb341 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:07:08 +0100 Subject: [PATCH 151/281] Add API endpoint for /stats --- docs/rest-api.md | 4 ++++ freqtrade/rpc/api_server.py | 14 ++++++++++++++ scripts/rest_client.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index 7726ab875..9bb35ce91 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -127,6 +127,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `performance` | Show performance of each finished trade grouped by pair. | `balance` | Show account balance per currency. | `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `stats` | Display a summary of profit / loss reasons as well as average holding times. | `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. @@ -229,6 +230,9 @@ show_config start Start the bot if it's in the stopped state. +stats + Return the stats report (durations, sell-reasons). + status Get the status of open trades. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8c2c203e6..c86aa1fa7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -198,6 +198,8 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', view_func=self._profit, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/stats', 'stats', + view_func=self._stats, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', view_func=self._performance, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/status', 'status', @@ -417,6 +419,18 @@ class ApiServer(RPC): return jsonify(stats) + @require_login + @rpc_catch_errors + def _stats(self): + """ + Handler for /stats. + Returns a Object with "durations" and "sell_reasons" as keys. + """ + + stats = self._rpc_stats() + + return jsonify(stats) + @require_login @rpc_catch_errors def _performance(self): diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 268e81397..2232b8421 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -139,6 +139,13 @@ class FtRestClient(): """ return self._get("profit") + def stats(self): + """Return the stats report (durations, sell-reasons). + + :return: json object + """ + return self._get("stats") + def performance(self): """Return the performance of the different coins. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dc43474f..2daa32bc7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -559,6 +559,35 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li } +@pytest.mark.usefixtures("init_persistence") +def test_api_stats(botclient, mocker, ticker, fee, markets,): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client_get(client, f"{BASE_URI}/stats") + assert_response(rc, 200) + assert 'durations' in rc.json + assert 'sell_reasons' in rc.json + + create_mock_trades(fee) + + rc = client_get(client, f"{BASE_URI}/stats") + assert_response(rc, 200) + assert 'durations' in rc.json + assert 'sell_reasons' in rc.json + + assert 'wins' in rc.json['durations'] + assert 'losses' in rc.json['durations'] + assert 'draws' in rc.json['durations'] + + def test_api_performance(botclient, mocker, ticker, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From f047297995a3e8875382a1445bbb53d6a1024810 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:45:02 +0100 Subject: [PATCH 152/281] Improve wording, fix bug --- docs/includes/protections.md | 2 +- freqtrade/mixins/logging_mixin.py | 1 + freqtrade/optimize/backtesting.py | 2 +- .../plugins/protections/max_drawdown_protection.py | 5 ++++- tests/plugins/test_protections.py | 12 ++++++++++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index a8caf55b1..7378a590c 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -7,7 +7,7 @@ Protections will protect your strategy from unexpected events and market conditi All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. !!! Note - Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. + Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance. !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index 2e1c20a52..06935d5f6 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -1,4 +1,5 @@ from typing import Callable + from cachetools import TTLCache, cached diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5bb7eaf74..de9c52dad 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,6 +18,7 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds +from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager @@ -25,7 +26,6 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType -from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index f1c77d1d9..d54e6699b 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -54,7 +54,10 @@ class MaxDrawdown(IProtection): return False, None, None # Drawdown is always positive - drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + try: + drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + except ValueError: + return False, None, None if drawdown > self._max_allowed_drawdown: self.log_once( diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2ad03a97c..82b6e4500 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -304,6 +304,18 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + Trade.session.add(generate_mock_trade( + 'NEO/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + # No losing trade yet ... so max_drawdown will raise exception + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=500, min_ago_close=400, profit_rate=0.9, From de2cc9708dc5d6e9f9e0bfa83d11e31a31442ca6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:01:29 +0100 Subject: [PATCH 153/281] Fix test leakage --- tests/plugins/test_pairlocks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index db7d9f46f..bd103b21e 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -79,6 +79,7 @@ def test_PairLocks(use_db): # Nothing was pushed to the database assert len(PairLock.query.all()) == 0 # Reset use-db variable + PairLocks.reset_locks() PairLocks.use_db = True @@ -111,4 +112,5 @@ def test_PairLocks_getlongestlock(use_db): # Must be longer than above assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=14) + PairLocks.reset_locks() PairLocks.use_db = True From b5289d5f0ed48ba5cbbb916ca30a388619bf62e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:02:55 +0100 Subject: [PATCH 154/281] Update full config with correct protection keys --- config_full.json.example | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 737015b41..b6170bceb 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -78,26 +78,26 @@ "protections": [ { "method": "StoplossGuard", - "lookback_period": 60, + "lookback_period_candles": 60, "trade_limit": 4, - "stopduration": 60 + "stop_duration_candles": 60 }, { "method": "CooldownPeriod", - "stopduration": 20 + "stop_duration_candles": 20 }, { "method": "MaxDrawdown", - "lookback_period": 2000, + "lookback_period_candles": 200, "trade_limit": 20, - "stop_duration": 10, + "stop_duration_candles": 10, "max_allowed_drawdown": 0.2 }, { "method": "LowProfitPairs", - "lookback_period": 360, + "lookback_period_candles": 360, "trade_limit": 1, - "stop_duration": 2, + "stop_duration_candles": 2, "required_profit": 0.02 } ], From c37bc307e29d26d9db8ceec2dd3e920354dafca2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:07:00 +0100 Subject: [PATCH 155/281] Small finetunings to documentation --- docs/developer.md | 2 +- freqtrade/pairlist/pairlistmanager.py | 3 --- freqtrade/persistence/models.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 48b021027..dcbaa3ca9 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -229,7 +229,7 @@ The method `global_stop()` will be called whenever a trade closed (sell order co ##### Protections - calculating lock end time Protections should calculate the lock end time based on the last trade it considers. -This avoids relocking should the lookback-period be longer than the actual lock period. +This avoids re-locking should the lookback-period be longer than the actual lock period. The `IProtection` parent class provides a helper method for this in `calculate_lock_end()`. diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 89bab99be..810a22300 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -26,9 +26,6 @@ class PairListManager(): self._pairlist_handlers: List[IPairList] = [] self._tickers_needed = False for pairlist_handler_config in self._config.get('pairlists', None): - if 'method' not in pairlist_handler_config: - logger.warning(f"No method found in {pairlist_handler_config}, ignoring.") - continue pairlist_handler = PairListResolver.load_pairlist( pairlist_handler_config['method'], exchange=exchange, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 07f4b5a4f..bcda6368a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -419,7 +419,7 @@ class Trade(_DECL_BASE): raise ValueError(f'Unknown order type: {order_type}') cleanup_db() - def close(self, rate: float, *, show_msg: bool = False) -> None: + def close(self, rate: float, *, show_msg: bool = True) -> None: """ Sets close_rate to the given rate, calculates total profit and marks trade as closed From 82bc6973feab3937010e99cb2301cbad30651724 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:16:33 +0100 Subject: [PATCH 156/281] Add last key to config_full --- config_full.json.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index b6170bceb..e69e52469 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -80,7 +80,8 @@ "method": "StoplossGuard", "lookback_period_candles": 60, "trade_limit": 4, - "stop_duration_candles": 60 + "stop_duration_candles": 60, + "only_per_pair": false }, { "method": "CooldownPeriod", From f897b683c7ec3031cc0aeb6dd863c5891a6e86c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 19:22:14 +0100 Subject: [PATCH 157/281] Add seperate page describing plugins --- docs/plugins.md | 3 +++ mkdocs.yml | 1 + 2 files changed, 4 insertions(+) create mode 100644 docs/plugins.md diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..1f785bbaa --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,3 @@ +# Plugins +--8<-- "includes/pairlists.md" +--8<-- "includes/protections.md" diff --git a/mkdocs.yml b/mkdocs.yml index c791386ae..a7ae0cc96 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Edge Positioning: edge.md + - Plugins: plugins.md - Utility Subcommands: utils.md - FAQ: faq.md - Data Analysis: From 9725b8e17cebe927ea2a0faa5f43e302c4cc18dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Dec 2020 08:43:22 +0100 Subject: [PATCH 158/281] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 68b37afe3..ea94822c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.0-slim-buster +FROM python:3.9.1-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From 118a22d0104cf6f8298f0f7c72639c450f0413b0 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 8 Dec 2020 18:04:26 +0100 Subject: [PATCH 159/281] Update data-download.md --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index e9c5c1865..2d77a8a17 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"` Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. -You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101`). For incremental downloads, the relative approach should be used. +You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used. !!! Tip "Tip: Updating existing data" If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. From d9a86158f4ef44b5f172e4b3af7c257fd7d5c85f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Dec 2020 19:46:54 +0100 Subject: [PATCH 160/281] Add cmake to support raspberry 64bit installs --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 5cc0e03f4..f00bf0836 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -112,7 +112,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` bash - sudo apt-get install python3-venv libatlas-base-dev + sudo apt-get install python3-venv libatlas-base-dev cmake # Use pywheels.org to speed up installation sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf From e6b3e645340da773d20f3b96144872b4bd115c63 Mon Sep 17 00:00:00 2001 From: David Martinez Martin Date: Wed, 9 Dec 2020 03:27:59 +0100 Subject: [PATCH 161/281] Update dockerfile to multistage This change reduce the image size from 727Mb to 469Mb. --- Dockerfile | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2be65274e..8840a707a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,40 @@ -FROM python:3.8.6-slim-buster +FROM python:3.8.6-slim-buster as base -RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev sqlite3 \ - && apt-get clean \ - && pip install --upgrade pip +# Setup env +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONFAULTHANDLER 1 # Prepare environment RUN mkdir /freqtrade WORKDIR /freqtrade +# Install dependencies +FROM base as python-deps +RUN apt-get update \ + && apt-get -y install git curl build-essential libssl-dev \ + && 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 requirements.txt requirements-hyperopt.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements-hyperopt.txt --no-cache-dir +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 /root/.local /root/.local +ENV PATH=/root/.local/bin:$PATH + # Install and execute COPY . /freqtrade/ From f1af2972e2b23b627a99e5a3645031e489066470 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 07:52:58 +0100 Subject: [PATCH 162/281] Ensure non-defined attributes fail correctly Remove unnecessary check, as stoploss cannot be none (it's mandatory and a number) --- freqtrade/freqtradebot.py | 3 +-- freqtrade/resolvers/strategy_resolver.py | 27 +++++++++++++++++------- freqtrade/strategy/interface.py | 3 +-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8d281852..15aa3416c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -519,8 +519,7 @@ class FreqtradeBot: # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent', constants.DEFAULT_AMOUNT_RESERVE_PERCENT) - if self.strategy.stoploss is not None: - amount_reserve_percent += self.strategy.stoploss + amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% amount_reserve_percent = max(amount_reserve_percent, 0.5) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 63a3f784e..73af00fee 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -88,9 +88,6 @@ class StrategyResolver(IResolver): StrategyResolver._override_attribute_helper(strategy, config, attribute, default) - # Assign deprecated variable - to not break users code relying on this. - strategy.ticker_interval = strategy.timeframe - # Loop this list again to have output combined for attribute, _, subkey in attributes: if subkey and attribute in config[subkey]: @@ -98,11 +95,7 @@ class StrategyResolver(IResolver): elif attribute in config: logger.info("Strategy using %s: %s", attribute, config[attribute]) - # Sort and apply type conversions - strategy.minimal_roi = OrderedDict(sorted( - {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), - key=lambda t: t[0])) - strategy.stoploss = float(strategy.stoploss) + StrategyResolver._normalize_attributes(strategy) StrategyResolver._strategy_sanity_validations(strategy) return strategy @@ -131,6 +124,24 @@ class StrategyResolver(IResolver): setattr(strategy, attribute, default) config[attribute] = default + @staticmethod + def _normalize_attributes(strategy: IStrategy) -> IStrategy: + """ + Normalize attributes to have the correct type. + """ + # Assign deprecated variable - to not break users code relying on this. + if hasattr(strategy, 'timeframe'): + strategy.ticker_interval = strategy.timeframe + + # Sort and apply type conversions + if hasattr(strategy, 'minimal_roi'): + strategy.minimal_roi = OrderedDict(sorted( + {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), + key=lambda t: t[0])) + if hasattr(strategy, 'stoploss'): + strategy.stoploss = float(strategy.stoploss) + return strategy + @staticmethod def _strategy_sanity_validations(strategy): if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 81f4e7651..125211a85 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -551,8 +551,7 @@ class IStrategy(ABC): # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. - if ((self.stoploss is not None) and - (trade.stop_loss >= current_rate) and + if ((trade.stop_loss >= current_rate) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS From 57080982566e295747da2286d87f94f012e812cc Mon Sep 17 00:00:00 2001 From: David Martinez Martin Date: Wed, 9 Dec 2020 10:34:38 +0100 Subject: [PATCH 163/281] Move ENV PATH to base image --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8840a707a..f85dfb0c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 +ENV PATH=/root/.local/bin:$PATH # Prepare environment RUN mkdir /freqtrade @@ -13,7 +14,7 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps RUN apt-get update \ - && apt-get -y install git curl build-essential libssl-dev \ + && apt-get -y install curl build-essential libssl-dev \ && apt-get clean \ && pip install --upgrade pip @@ -33,7 +34,7 @@ COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib COPY --from=python-deps /root/.local /root/.local -ENV PATH=/root/.local/bin:$PATH + # Install and execute From 25f8e0cc57b050c10ecab3cbf1d2b712008fd341 Mon Sep 17 00:00:00 2001 From: David Martinez Martin Date: Wed, 9 Dec 2020 11:28:45 +0100 Subject: [PATCH 164/281] Added git packages for future dependencies --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f85dfb0c7..602e6a28c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev \ + && apt-get -y install curl build-essential libssl-dev git \ && apt-get clean \ && pip install --upgrade pip From f5817063b75b5957d29783c3f5f1b9eda19d20fc Mon Sep 17 00:00:00 2001 From: Samaoo Date: Wed, 9 Dec 2020 15:53:38 +0100 Subject: [PATCH 165/281] Update backtesting.md --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 3058d1b57..1fc9f3d73 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) -- Stoploss sells happen exactly at stoploss price, even if low was lower +- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be 0.32% lower than the stoploss price. - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss From af53dfbfab71e61cc3c50200ed2bb26c283720b7 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Wed, 9 Dec 2020 15:57:15 +0100 Subject: [PATCH 166/281] Update backtesting.md --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 1fc9f3d73..de54c4c91 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) -- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be 0.32% lower than the stoploss price. +- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be 0.32% lower than the stoploss price - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss From 33f330256b02098458cbd18e28d970a735efb5e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 20:26:11 +0100 Subject: [PATCH 167/281] Reorder commands on telegram init --- freqtrade/rpc/telegram.py | 2 +- tests/conftest_trades.py | 10 ++++++++-- tests/rpc/test_rpc_telegram.py | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b6c0a1f3f..fa36cfee9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -99,6 +99,7 @@ class Telegram(RPC): CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), + CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), @@ -111,7 +112,6 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), - CommandHandler('stats', self._stats), ] for handle in handles: self._updater.dispatcher.add_handler(handle) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index fac822b2b..e84722041 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta, timezone + from freqtrade.persistence.models import Order, Trade @@ -82,7 +84,9 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', - sell_reason='sell_signal' + sell_reason='sell_signal', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc), ) o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) @@ -135,7 +139,9 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, - sell_reason='roi' + sell_reason='roi', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc), ) o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 725c1411e..ecad05683 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -74,10 +74,10 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['daily'], ['count'], ['locks'], " + "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version'], " - "['stats']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" + "]") assert log_has(message_str, caplog) From ca99d484fcd852561f8acbb3bd9cbb879ddc724d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 07:39:50 +0100 Subject: [PATCH 168/281] Refactor to use list comprehension --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fa36cfee9..c54000677 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -398,7 +398,6 @@ class Telegram(RPC): """ stats = self._rpc_stats() - sell_reasons_tabulate = [] reason_map = { 'roi': 'ROI', 'stop_loss': 'Stoploss', @@ -408,13 +407,14 @@ class Telegram(RPC): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - for reason, count in stats['sell_reasons'].items(): - sell_reasons_tabulate.append([ + sell_reasons_tabulate = [ + [ reason_map.get(reason, reason), sum(count.values()), count['wins'], count['losses'] - ]) + ] for reason, count in stats['sell_reasons'].items() + ] sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] From 201cc67e0503c98ad962ea1a0a53b4763fdedd81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 19:21:20 +0100 Subject: [PATCH 169/281] Rename open_trade_price to "open_trade_value" --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/models.py | 22 +++++++++++----------- tests/rpc/test_rpc.py | 4 ++-- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/test_persistence.py | 28 ++++++++++++++-------------- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8d281852..59da58e1b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1396,7 +1396,7 @@ class FreqtradeBot: abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount order.pop('filled', None) - trade.recalc_open_trade_price() + trade.recalc_open_trade_value() except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6027908da..67871f96b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -217,7 +217,7 @@ class Trade(_DECL_BASE): fee_close_currency = Column(String, nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) - # open_trade_price - calculated via _calc_open_trade_price + # open_trade_price - calculated via _calc_open_trade_value open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) @@ -252,7 +252,7 @@ class Trade(_DECL_BASE): def __init__(self, **kwargs): super().__init__(**kwargs) - self.recalc_open_trade_price() + self.recalc_open_trade_value() def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' @@ -284,7 +284,7 @@ class Trade(_DECL_BASE): 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, - 'open_trade_price': round(self.open_trade_price, 8), + 'open_trade_value': round(self.open_trade_price, 8), 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), @@ -389,7 +389,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) - self.recalc_open_trade_price() + self.recalc_open_trade_value() if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None @@ -464,7 +464,7 @@ class Trade(_DECL_BASE): Trade.session.delete(self) Trade.session.flush() - def _calc_open_trade_price(self) -> float: + def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees @@ -473,14 +473,14 @@ class Trade(_DECL_BASE): fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) - def recalc_open_trade_price(self) -> None: + def recalc_open_trade_value(self) -> None: """ - Recalculate open_trade_price. + Recalculate open_trade_value. Must be called whenever open_rate or fee_open is changed. """ - self.open_trade_price = self._calc_open_trade_price() + self.open_trade_price = self._calc_open_trade_value() - def calc_close_trade_price(self, rate: Optional[float] = None, + def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ Calculate the close_rate including fee @@ -507,7 +507,7 @@ class Trade(_DECL_BASE): If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - close_trade_price = self.calc_close_trade_price( + close_trade_price = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) @@ -523,7 +523,7 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit ratio as float """ - close_trade_price = self.calc_close_trade_price( + close_trade_price = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 47e0f763d..4b36f4b4e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -62,7 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, - 'open_trade_price': 0.0010025, + 'open_trade_value': 0.0010025, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, @@ -127,7 +127,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, - 'open_trade_price': ANY, + 'open_trade_value': ANY, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dc43474f..8e5a66998 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -678,7 +678,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'min_rate': 1.098e-05, 'open_order_id': None, 'open_rate_requested': 1.098e-05, - 'open_trade_price': 0.0010025, + 'open_trade_value': 0.0010025, 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', @@ -805,7 +805,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'min_rate': None, 'open_order_id': '123456', 'open_rate_requested': None, - 'open_trade_price': 0.24605460, + 'open_trade_value': 0.24605460, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 41b99b34f..a7ac8ed94 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -177,10 +177,10 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade.open_order_id = 'something' trade.update(limit_buy_order) - assert trade._calc_open_trade_price() == 0.0010024999999225068 + assert trade._calc_open_trade_value() == 0.0010024999999225068 trade.update(limit_sell_order) - assert trade.calc_close_trade_price() == 0.0010646656050132426 + assert trade.calc_close_trade_value() == 0.0010646656050132426 # Profit in BTC assert trade.calc_profit() == 0.00006217 @@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): trade.open_order_id = 'something' trade.update(limit_buy_order) - assert trade.calc_close_trade_price() == 0.0 + assert trade.calc_close_trade_value() == 0.0 @pytest.mark.usefixtures("init_persistence") @@ -277,7 +277,7 @@ def test_update_invalid_order(limit_buy_order): @pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_price(limit_buy_order, fee): +def test_calc_open_trade_value(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -291,10 +291,10 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_price() == 0.0010024999999225068 + assert trade._calc_open_trade_value() == 0.0010024999999225068 trade.fee_open = 0.003 # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_price() == 0.001002999999922468 + assert trade._calc_open_trade_value() == 0.001002999999922468 @pytest.mark.usefixtures("init_persistence") @@ -312,14 +312,14 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get the close rate price with a custom close rate and a regular fee rate - assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318470471794 + assert trade.calc_close_trade_value(rate=0.00001234) == 0.0011200318470471794 # Get the close rate price with a custom close rate and a custom fee rate - assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704275749754 + assert trade.calc_close_trade_value(rate=0.00001234, fee=0.003) == 0.0011194704275749754 # Test when we apply a Sell order, and ask price with a custom fee rate trade.update(limit_sell_order) - assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972701635854 + assert trade.calc_close_trade_value(fee=0.005) == 0.0010619972701635854 @pytest.mark.usefixtures("init_persistence") @@ -499,7 +499,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_price == trade._calc_open_trade_price() + assert trade.open_trade_price == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -607,7 +607,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2", caplog) - assert trade.open_trade_price == trade._calc_open_trade_price() + assert trade.open_trade_price == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert log_has("Moving open orders to Orders table.", caplog) @@ -677,7 +677,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_price == trade._calc_open_trade_price() + assert trade.open_trade_price == trade._calc_open_trade_value() assert log_has("trying trades_bak0", caplog) assert log_has("Running database migration for trades - backup: trades_bak0", caplog) @@ -803,7 +803,7 @@ def test_to_json(default_conf, fee): 'close_timestamp': None, 'open_rate': 0.123, 'open_rate_requested': None, - 'open_trade_price': 15.1668225, + 'open_trade_value': 15.1668225, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, @@ -896,7 +896,7 @@ def test_to_json(default_conf, fee): 'min_rate': None, 'open_order_id': None, 'open_rate_requested': None, - 'open_trade_price': 12.33075, + 'open_trade_value': 12.33075, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, From 95fd3824daa02d6889b7eaf636a51397d37f0f59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 19:36:52 +0100 Subject: [PATCH 170/281] Finish renamal of open_trade_price to open_value --- freqtrade/persistence/migrations.py | 10 +++++----- freqtrade/persistence/models.py | 16 ++++++++-------- tests/test_persistence.py | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 84f3ed7e6..ed976c2a9 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -53,11 +53,11 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col else: timeframe = get_column_def(cols, 'timeframe', 'null') - open_trade_price = get_column_def(cols, 'open_trade_price', + open_trade_value = get_column_def(cols, 'open_trade_value', f'amount * open_rate * (1 + {fee_open})') close_profit_abs = get_column_def( cols, 'close_profit_abs', - f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") + f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") sell_order_status = get_column_def(cols, 'sell_order_status', 'null') amount_requested = get_column_def(cols, 'amount_requested', 'amount') @@ -79,7 +79,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, - timeframe, open_trade_price, close_profit_abs + timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), case @@ -102,7 +102,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, - {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs + {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} """) @@ -134,7 +134,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'amount_requested'): + if not has_column(cols, 'open_trade_value'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 67871f96b..06dd785e8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -217,8 +217,8 @@ class Trade(_DECL_BASE): fee_close_currency = Column(String, nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) - # open_trade_price - calculated via _calc_open_trade_value - open_trade_price = Column(Float) + # open_trade_value - calculated via _calc_open_trade_value + open_trade_value = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) @@ -284,7 +284,7 @@ class Trade(_DECL_BASE): 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, - 'open_trade_value': round(self.open_trade_price, 8), + 'open_trade_value': round(self.open_trade_value, 8), 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), @@ -478,7 +478,7 @@ class Trade(_DECL_BASE): Recalculate open_trade_value. Must be called whenever open_rate or fee_open is changed. """ - self.open_trade_price = self._calc_open_trade_value() + self.open_trade_value = self._calc_open_trade_value() def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: @@ -507,11 +507,11 @@ class Trade(_DECL_BASE): If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - close_trade_price = self.calc_close_trade_value( + close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_price - self.open_trade_price + profit = close_trade_value - self.open_trade_value return float(f"{profit:.8f}") def calc_profit_ratio(self, rate: Optional[float] = None, @@ -523,11 +523,11 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit ratio as float """ - close_trade_price = self.calc_close_trade_value( + close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit_ratio = (close_trade_price / self.open_trade_price) - 1 + profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a7ac8ed94..7487b2ef5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -499,7 +499,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_price == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -607,7 +607,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2", caplog) - assert trade.open_trade_price == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert log_has("Moving open orders to Orders table.", caplog) @@ -677,7 +677,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_price == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value() assert log_has("trying trades_bak0", caplog) assert log_has("Running database migration for trades - backup: trades_bak0", caplog) From 6107878f4e573b6f0b00eb56f9d0704408a2a806 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 07:08:29 +0100 Subject: [PATCH 171/281] Bump ccxt to 1.39.10 closes #4051 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 105839f0d..e0c5ac072 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.38.87 +ccxt==1.39.10 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From b45c2fb1d015847ba0b3fbe1c8059d7267e99d6a Mon Sep 17 00:00:00 2001 From: Samaoo Date: Sat, 12 Dec 2020 10:27:17 +0100 Subject: [PATCH 172/281] Update backtesting.md --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index de54c4c91..27bfebe37 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) -- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be 0.32% lower than the stoploss price +- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss From 181b88dc753354b34ca65162541823e1bf7b5258 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 10:52:27 +0100 Subject: [PATCH 173/281] Don't accept too high fees, assuming they are erroneous Forces fallback to "detection from trades" --- freqtrade/freqtradebot.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ada9889a6..c86fb616b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1448,13 +1448,16 @@ class FreqtradeBot: fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") - - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - if trade_base_currency == fee_currency: - # Apply fee to amount - return self.apply_fee_conditional(trade, trade_base_currency, - amount=order_amount, fee_abs=fee_cost) - return order_amount + if fee_rate is None or fee_rate < 0.02: + # Reject all fees that report as > 2%. + # These are most likely caused by a parsing bug in ccxt + # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025) + trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) + if trade_base_currency == fee_currency: + # Apply fee to amount + return self.apply_fee_conditional(trade, trade_base_currency, + amount=order_amount, fee_abs=fee_cost) + return order_amount return self.fee_detection_from_trades(trade, order, order_amount) def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: From 3ee7fe64ba3e957ff5110f07b06c3f7265060a1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 11:25:56 +0100 Subject: [PATCH 174/281] Clean up some tests --- tests/conftest.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 079a521ed..e2e4788b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1588,16 +1588,7 @@ def fetch_trades_result(): @pytest.fixture(scope="function") def trades_for_order2(): - return [{'info': {'id': 34567, - 'orderId': 123456, - 'price': '0.24544100', - 'qty': '8.00000000', - 'commission': '0.00800000', - 'commissionAsset': 'LTC', - 'time': 1521663363189, - 'isBuyer': True, - 'isMaker': False, - 'isBestMatch': True}, + return [{'info': {}, 'timestamp': 1521663363189, 'datetime': '2018-03-21T20:16:03.189Z', 'symbol': 'LTC/ETH', @@ -1609,16 +1600,7 @@ def trades_for_order2(): 'cost': 1.963528, 'amount': 4.0, 'fee': {'cost': 0.004, 'currency': 'LTC'}}, - {'info': {'id': 34567, - 'orderId': 123456, - 'price': '0.24544100', - 'qty': '8.00000000', - 'commission': '0.00800000', - 'commissionAsset': 'LTC', - 'time': 1521663363189, - 'isBuyer': True, - 'isMaker': False, - 'isBestMatch': True}, + {'info': {}, 'timestamp': 1521663363189, 'datetime': '2018-03-21T20:16:03.189Z', 'symbol': 'LTC/ETH', From 14647fb5f08254d4b85c528f59db66c697c43d83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 11:43:47 +0100 Subject: [PATCH 175/281] Add tests for update fee --- tests/conftest.py | 8 +++++++ tests/test_freqtradebot.py | 44 +++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e2e4788b6..5d358f015 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1614,6 +1614,14 @@ def trades_for_order2(): 'fee': {'cost': 0.004, 'currency': 'LTC'}}] +@pytest.fixture(scope="function") +def trades_for_order3(trades_for_order2): + # Different fee currencies for each trade + trades_for_order = deepcopy(trades_for_order2) + trades_for_order[0]['fee'] = {'cost': 0.02, 'currency': 'BNB'} + return trades_for_order + + @pytest.fixture def buy_order_fee(): return { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6adef510f..459a09c0c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3718,6 +3718,48 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', caplog) + assert trade.fee_open == 0.001 + assert trade.fee_close == 0.001 + assert trade.fee_open_cost is not None + assert trade.fee_open_currency is not None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + + +def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee, caplog, fee, + mocker, markets): + # Different fee currency on both trades + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order3) + amount = float(sum(x['amount'] for x in trades_for_order3)) + default_conf['stake_currency'] = 'ETH' + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.245441, + open_order_id="123456" + ) + # Fake markets entry to enable fee parsing + markets['BNB/ETH'] = markets['ETH/BTC'] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': 0.19, 'last': 0.2}) + + # Amount is reduced by "fee" + assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005) + assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', + caplog) + # Overall fee is average of both trade's fee + assert trade.fee_open == 0.001518575 + assert trade.fee_open_cost is not None + assert trade.fee_open_currency is not None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): @@ -4264,7 +4306,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) def patch_with_fee(order): - order.update({'fee': {'cost': 0.1, 'rate': 0.2, + order.update({'fee': {'cost': 0.1, 'rate': 0.01, 'currency': order['symbol'].split('/')[0]}}) return order From 8a2fbf65923283efd0db0c61c316cc5df193089f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:15:16 +0100 Subject: [PATCH 176/281] Small cleanup of protection stuff --- docs/includes/protections.md | 12 ++++++------ freqtrade/resolvers/protection_resolver.py | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 7378a590c..87db17fd8 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -26,12 +26,12 @@ All protection end times are rounded up to the next candle to avoid sudden, unex | Parameter| Description | |------------|-------------| -| method | Protection name to use.
**Datatype:** String, selected from [available Protections](#available-protections) -| stop_duration_candles | For how many candles should the lock be set?
**Datatype:** Positive integer (in candles) -| stop_duration | how many minutes should protections be locked.
Cannot be used together with `stop_duration_candles`.
**Datatype:** Float (in minutes) +| `method` | Protection name to use.
**Datatype:** String, selected from [available Protections](#available-protections) +| `stop_duration_candles` | For how many candles should the lock be set?
**Datatype:** Positive integer (in candles) +| `stop_duration` | how many minutes should protections be locked.
Cannot be used together with `stop_duration_candles`.
**Datatype:** Float (in minutes) | `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections.
**Datatype:** Positive integer (in candles). -| lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
Cannot be used together with `lookback_period_candles`.
This setting may be ignored by some Protections.
**Datatype:** Float (in minutes) -| trade_limit | Number of trades required at minimum (not used by all Protections).
**Datatype:** Positive integer +| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered.
Cannot be used together with `lookback_period_candles`.
This setting may be ignored by some Protections.
**Datatype:** Float (in minutes) +| `trade_limit` | Number of trades required at minimum (not used by all Protections).
**Datatype:** Positive integer !!! Note "Durations" Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles). @@ -108,7 +108,7 @@ The below example will stop trading a pair for 2 candles after closing a trade, "protections": [ { "method": "CooldownPeriod", - "stop_duration_candle": 2 + "stop_duration_candles": 2 } ], ``` diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py index 928bd4633..c54ae1011 100644 --- a/freqtrade/resolvers/protection_resolver.py +++ b/freqtrade/resolvers/protection_resolver.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=attribute-defined-outside-init - """ This module load custom pairlists """ From 9cd1be8f93f59b332d6317fc777033343ce39036 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:33:45 +0100 Subject: [PATCH 177/281] Update usage of open_trade_price to open_trade_value --- tests/plugins/test_protections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 82b6e4500..e36900a96 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -29,7 +29,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, amount=0.01 / open_rate, exchange='bittrex', ) - trade.recalc_open_trade_price() + trade.recalc_open_trade_value() if not is_open: trade.close(open_rate * profit_rate) trade.sell_reason = sell_reason From 657b002a8197b2a949ac423f7207a379db5bd787 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:59:29 +0100 Subject: [PATCH 178/281] Explicitly check for False in fetch_ticker --- freqtrade/exchange/exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 611ce4abd..7f763610c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -658,7 +658,8 @@ class Exchange: @retrier def fetch_ticker(self, pair: str) -> dict: try: - if pair not in self._api.markets or not self._api.markets[pair].get('active'): + if (pair not in self._api.markets or + self._api.markets[pair].get('active', False) is False): raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data From a4bfd0b0aa6556e47484fc67a675170b9ddd760e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 11:25:42 +0100 Subject: [PATCH 179/281] Split linux and OSX builds into 2 seperate, parallel jobs --- .github/workflows/ci.yml | 102 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d48dec2d3..36a9fc374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,12 @@ on: - cron: '0 5 * * 4' jobs: - build: + build_linux: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] + os: [ ubuntu-18.04, ubuntu-20.04 ] python-version: [3.7, 3.8, 3.9] steps: @@ -31,21 +31,105 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache_dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 id: cache with: path: ~/dependencies/ key: ${{ runner.os }}-dependencies - name: pip cache (linux) - uses: actions/cache@preview + uses: actions/cache@v2 if: startsWith(matrix.os, 'ubuntu') with: path: ~/.cache/pip key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip + - name: TA binary *nix + if: steps.cache.outputs.cache-hit != 'true' + run: | + cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. + + - name: Installation - *nix + run: | + python -m pip install --upgrade pip + export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH + export TA_LIBRARY_PATH=${HOME}/dependencies/lib + export TA_INCLUDE_PATH=${HOME}/dependencies/include + pip install -r requirements-dev.txt + pip install -e . + + - name: Tests + run: | + pytest --random-order --cov=freqtrade --cov-config=.coveragerc + + - name: Coveralls + if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8') + env: + # Coveralls token. Not used as secret due to github not providing secrets to forked repositories + COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu + run: | + # Allow failure for coveralls + coveralls -v || true + + - name: Backtesting + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy + + - name: Hyperopt + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all + + - name: Flake8 + run: | + flake8 + + - name: Sort imports (isort) + run: | + isort --check . + + - name: Mypy + run: | + mypy freqtrade scripts + + - name: Slack Notification + uses: homoluctus/slatify@v1.8.0 + if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI ${{ matrix.os }}*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + + build_macos: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ macos-latest ] + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache_dependencies + uses: actions/cache@v2 + id: cache + with: + path: ~/dependencies/ + key: ${{ runner.os }}-dependencies + - name: pip cache (macOS) - uses: actions/cache@preview + uses: actions/cache@v2 if: startsWith(matrix.os, 'macOS') with: path: ~/Library/Caches/pip @@ -113,13 +197,14 @@ jobs: channel: '#notifications' url: ${{ secrets.SLACK_WEBHOOK }} + build_windows: runs-on: ${{ matrix.os }} strategy: matrix: os: [ windows-latest ] - python-version: [3.7, 3.8, 3.9] + python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2 @@ -215,7 +300,7 @@ jobs: # Notify on slack only once - when CI completes (and after deploy) in case it's successfull notify-complete: - needs: [ build, build_windows, docs_check ] + needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: - name: Slack Notification @@ -228,8 +313,9 @@ jobs: url: ${{ secrets.SLACK_WEBHOOK }} deploy: - needs: [ build, build_windows, docs_check ] + needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 + if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' steps: - uses: actions/checkout@v2 From dad427461d88ea70697f42b91c136ba9284b8f6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 13:11:04 +0100 Subject: [PATCH 180/281] Downgrade dockerfile to 3.8.6 to avoid image bloat --- Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 445f909b0..602e6a28c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.1-slim-buster as base +FROM python:3.8.6-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/docker-compose.yml b/docker-compose.yml index a99aac3c7..7094500b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: # Build step - only needed when additional dependencies are needed # build: # context: . - # dockerfile: "./Dockerfile.technical" + # dockerfile: "./docker/Dockerfile.technical" restart: unless-stopped container_name: freqtrade volumes: From 3bea9255e78bb04bf1fad25e165513b322acb0d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:33 +0000 Subject: [PATCH 181/281] Bump cachetools from 4.1.1 to 4.2.0 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.1.1 to 4.2.0. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v4.1.1...v4.2.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..a30c5e8b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 arrow==0.17.0 -cachetools==4.1.1 +cachetools==4.2.0 requests==2.25.0 urllib3==1.26.2 wrapt==1.12.1 From 4cf16fa8d1e3299857903aa4486c2a4d5efe5102 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:33 +0000 Subject: [PATCH 182/281] Bump plotly from 4.13.0 to 4.14.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.13.0 to 4.14.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.13.0...v4.14.1) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 1c3b03133..3e31a24ae 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.13.0 +plotly==4.14.1 From a3139dd9d416e26163cab5aedd392d0e674664e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:34 +0000 Subject: [PATCH 183/281] Bump flake8-tidy-imports from 4.1.0 to 4.2.0 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.1.0...4.2.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e681274c8..97bb7a12f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.1.0 +flake8-tidy-imports==4.2.0 mypy==0.790 pytest==6.1.2 pytest-asyncio==0.14.0 From bdd895b8dacddf59edfc91d9356228075aa1ccb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:42 +0000 Subject: [PATCH 184/281] Bump pandas from 1.1.4 to 1.1.5 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.4 to 1.1.5. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.4...v1.1.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..47fd5006b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.4 -pandas==1.1.4 +pandas==1.1.5 ccxt==1.39.10 aiohttp==3.7.3 From 44f295110bc10875bc4b571d92d485aca5d59c8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:46 +0000 Subject: [PATCH 185/281] Bump python-rapidjson from 0.9.4 to 1.0 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.9.4 to 1.0. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v0.9.4...v1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..ae09adabd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.4 +python-rapidjson==1.0 # Notify systemd sdnotify==0.3.2 From a9b586d338ca2f9bb86a0e03ff8398b8d241d39d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:44:16 +0000 Subject: [PATCH 186/281] Bump ccxt from 1.39.10 to 1.39.33 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.10 to 1.39.33. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.10...1.39.33) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..358238ea6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.39.10 +ccxt==1.39.33 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From 8965b8a18d7390929b38d33f829f5ffdf87772b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 08:27:36 +0000 Subject: [PATCH 187/281] Bump pytest from 6.1.2 to 6.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.2 to 6.2.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.1.2...6.2.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 97bb7a12f..6d7570f67 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.0 mypy==0.790 -pytest==6.1.2 +pytest==6.2.0 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From ba869a330f5b8e707cfa1419c0a36ce31147c71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Dec 2020 19:05:41 +0100 Subject: [PATCH 188/281] Build 3.6 on github actions too --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f259129d4..9239b83ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] - python-version: [3.7, 3.8] + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 From 66d5271adae8db286ab67f9821638eabdc39e547 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Dec 2020 19:10:24 +0100 Subject: [PATCH 189/281] Don't build for 3.6 any longer --- .github/workflows/ci.yml | 2 +- .travis.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9239b83ee..f259129d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] - python-version: [3.6, 3.7, 3.8] + python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2 diff --git a/.travis.yml b/.travis.yml index 9b8448db5..b61efe678 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ os: - linux -dist: xenial +dist: bionic language: python python: -- 3.6 +- 3.7 +- 3.8 services: - docker env: From 9f5c4ead15993e67902c094109b3163a3626eafb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Dec 2020 19:18:54 +0100 Subject: [PATCH 190/281] Remove support for 3.6 --- .readthedocs.yml | 4 ++-- .travis.yml | 1 - README.md | 4 ++-- docs/index.md | 4 ++-- docs/installation.md | 8 ++++---- environment.yml | 2 +- freqtrade/__main__.py | 2 +- freqtrade/main.py | 4 ++-- setup.py | 5 ++--- setup.sh | 19 ++++++++++--------- 10 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index dec7b44d7..446181452 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,5 +4,5 @@ build: image: latest python: - version: 3.6 - setup_py_install: false \ No newline at end of file + version: 3.8 + setup_py_install: false diff --git a/.travis.yml b/.travis.yml index b61efe678..94239e33f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ os: dist: bionic language: python python: -- 3.7 - 3.8 services: - docker diff --git a/README.md b/README.md index 8526b5c91..a9aee342f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io ## Features -- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux. +- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Persistence**: Persistence is achieved through sqlite. - [x] **Dry-run**: Run the bot without playing money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. @@ -187,7 +187,7 @@ To run this bot we recommend you a cloud instance with a minimum of: ### Software requirements -- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [Python 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) diff --git a/docs/index.md b/docs/index.md index f63aeb6b8..e6882263b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ ## Introduction -Freqtrade is a crypto-currency algorithmic trading software developed in python (3.6+) and supported on Windows, macOS and Linux. +Freqtrade is a crypto-currency algorithmic trading software developed in python (3.7+) and supported on Windows, macOS and Linux. !!! Danger "DISCLAIMER" This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. @@ -51,7 +51,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of: Alternatively -- Python 3.6.x +- Python 3.7+ - pip (pip3) - git - TA-Lib diff --git a/docs/installation.md b/docs/installation.md index f00bf0836..be98c45a8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,7 +10,7 @@ Please consider using the prebuilt [docker images](docker.md) to get started qui Click each one for install guide: -* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) @@ -34,7 +34,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note - Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. This can be achieved with the following commands: @@ -63,7 +63,7 @@ usage: ** --install ** With this option, the script will install the bot and most dependencies: -You will need to have git and python3.6+ installed beforehand for this to work. +You will need to have git and python3.7+ installed beforehand for this to work. * Mandatory software as: `ta-lib` * Setup your virtualenv under `.env/` @@ -94,7 +94,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. !!! Note - Python3.6 or higher and the corresponding pip are assumed to be available. + Python3.7 or higher and the corresponding pip are assumed to be available. === "Ubuntu/Debian" #### Install necessary dependencies diff --git a/environment.yml b/environment.yml index 86ea03519..746c4b912 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: - conda-forge dependencies: # Required for app - - python>=3.6 + - python>=3.7 - pip - wheel - numpy diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 881a2f562..ab4c7a110 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -3,7 +3,7 @@ __main__.py for Freqtrade To launch Freqtrade as a module -> python -m freqtrade (with Python >= 3.6) +> python -m freqtrade (with Python >= 3.7) """ from freqtrade import main diff --git a/freqtrade/main.py b/freqtrade/main.py index 5f8d5d19d..84d4b24f8 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -9,8 +9,8 @@ from typing import Any, List # check min. python version -if sys.version_info < (3, 6): - sys.exit("Freqtrade requires Python version >= 3.6") +if sys.version_info < (3, 7): + sys.exit("Freqtrade requires Python version >= 3.7") from freqtrade.commands import Arguments from freqtrade.exceptions import FreqtradeException, OperationalException diff --git a/setup.py b/setup.py index b47427709..030980c96 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,9 @@ from sys import version_info from setuptools import setup -if version_info.major == 3 and version_info.minor < 6 or \ +if version_info.major == 3 and version_info.minor < 7 or \ version_info.major < 3: - print('Your Python interpreter must be 3.6 or greater!') + print('Your Python interpreter must be 3.7 or greater!') exit(1) from pathlib import Path # noqa: E402 @@ -109,7 +109,6 @@ setup(name='freqtrade', 'Environment :: Console', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Operating System :: MacOS', diff --git a/setup.sh b/setup.sh index af5e70691..d1686d2de 100755 --- a/setup.sh +++ b/setup.sh @@ -25,6 +25,14 @@ function check_installed_python() { return fi + which python3.9 + if [ $? -eq 0 ]; then + echo "using Python 3.9" + PYTHON=python3.9 + check_installed_pip + return + fi + which python3.7 if [ $? -eq 0 ]; then echo "using Python 3.7" @@ -33,16 +41,9 @@ function check_installed_python() { return fi - which python3.6 - if [ $? -eq 0 ]; then - echo "using Python 3.6" - PYTHON=python3.6 - check_installed_pip - return - fi if [ -z ${PYTHON} ]; then - echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" + echo "No usable python found. Please make sure to have python3.7 or greater installed" exit 1 fi } @@ -302,7 +303,7 @@ function help() { echo " -p,--plot Install dependencies for Plotting scripts." } -# Verify if 3.6 or 3.7 is installed +# Verify if 3.7 or 3.8 is installed check_installed_python case $* in From dc92808335745045f556b89e91df1f33cc7a7400 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 06:44:08 +0100 Subject: [PATCH 191/281] Change PI dockerfile to use staged build --- Dockerfile.armhf | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 0633008ea..822da00a5 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,25 +1,40 @@ -FROM --platform=linux/arm/v7 python:3.7.7-slim-buster +FROM --platform=linux/arm/v7 python:3.7.9-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=/root/.local/bin:$PATH + +# Prepare environment +RUN mkdir /freqtrade +WORKDIR /freqtrade + +# Install dependencies +FROM base as python-deps RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev libffi-dev libatlas3-base libgfortran5 sqlite3 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf -# Prepare environment -RUN mkdir /freqtrade -WORKDIR /freqtrade - # Install TA-lib COPY build_helpers/* /tmp/ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* - ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies COPY requirements.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements.txt --no-cache-dir +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-cache-dir -r requirements.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 /root/.local /root/.local # Install and execute COPY . /freqtrade/ From 39fec25ae09c6ff727d953497bd211b1b18e2857 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 08:22:45 +0100 Subject: [PATCH 192/281] add optional Cache arguments to refresh_pairs method --- freqtrade/exchange/exchange.py | 24 ++++++++++++++++-------- tests/exchange/test_exchange.py | 12 ++++++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7f763610c..b61049c4e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -733,13 +733,17 @@ class Exchange: logger.info("Downloaded data for %s with length %s.", pair, len(data)) return data - def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes) -> List[Tuple[str, List]]: + def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, + since_ms: Optional[int] = None, cache: bool = True + ) -> Dict[str, DataFrame]: """ Refresh in-memory OHLCV asynchronously and set `_klines` with the result Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). Only used in the dataprovider.refresh() method. :param pair_list: List of 2 element tuples containing pair, interval to refresh - :return: TODO: return value is only used in the tests, get rid of it + :param since_ms: time since when to download, in milliseconds + :param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists + :return: Dict of [{(pair, timeframe): Dataframe}] """ logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) @@ -749,7 +753,8 @@ class Exchange: for pair, timeframe in set(pair_list): if (not ((pair, timeframe) in self._klines) or self._now_is_time_to_refresh(pair, timeframe)): - input_coroutines.append(self._async_get_candle_history(pair, timeframe)) + input_coroutines.append(self._async_get_candle_history(pair, timeframe, + since_ms=since_ms)) else: logger.debug( "Using cached candle (OHLCV) data for pair %s, timeframe %s ...", @@ -759,6 +764,7 @@ class Exchange: results = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) + results_df = {} # handle caching for res in results: if isinstance(res, Exception): @@ -770,11 +776,13 @@ class Exchange: if ticks: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache - self._klines[(pair, timeframe)] = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=self._ohlcv_partial_candle) - - return results + ohlcv_df = ohlcv_to_dataframe( + ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) + results_df[(pair, timeframe)] = ohlcv_df + if cache: + self._klines[(pair, timeframe)] = ohlcv_df + return results_df def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: # Timeframe in seconds diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 42681b367..d8a846124 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1385,6 +1385,12 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] # empty dicts assert not exchange._klines + exchange.refresh_latest_ohlcv(pairs, cache=False) + # No caching + assert not exchange._klines + assert exchange._api_async.fetch_ohlcv.call_count == 2 + exchange._api_async.fetch_ohlcv.reset_mock() + exchange.refresh_latest_ohlcv(pairs) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) @@ -1499,11 +1505,9 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert exchange._klines assert exchange._api_async.fetch_ohlcv.call_count == 2 - assert type(res) is list - assert len(res) == 2 + assert type(res) is dict + assert len(res) == 1 # Test that each is in list at least once as order is not guaranteed - assert type(res[0]) is tuple or type(res[1]) is tuple - assert type(res[0]) is TypeError or type(res[1]) is TypeError assert log_has("Error loading ETH/BTC. Result was [[]].", caplog) assert log_has("Async code raised an exception: TypeError", caplog) From 69901c131405c4e8262ddf0452e4b13dea7baf41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 08:36:42 +0100 Subject: [PATCH 193/281] Provide pair to _validate_pairs in pairlists --- freqtrade/pairlist/AgeFilter.py | 5 +++-- freqtrade/pairlist/IPairList.py | 5 +++-- freqtrade/pairlist/PrecisionFilter.py | 9 +++++---- freqtrade/pairlist/PriceFilter.py | 11 ++++++----- freqtrade/pairlist/SpreadFilter.py | 7 ++++--- freqtrade/pairlist/rangestabilityfilter.py | 8 ++++---- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index ae2132637..f909014ba 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -49,11 +49,12 @@ class AgeFilter(IPairList): return (f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") - def _validate_pair(self, ticker: Dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Validate age for the ticker + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 5f29241ce..865aa90d6 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -60,13 +60,14 @@ class IPairList(LoggingMixin, ABC): -> Please overwrite in subclasses """ - def _validate_pair(self, ticker) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Check one pair against Pairlist Handler's specific conditions. Either implement it in the Pairlist Handler or override the generic filter_pairlist() method. + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ @@ -109,7 +110,7 @@ class IPairList(LoggingMixin, ABC): # Copy list since we're modifying this list for p in deepcopy(pairlist): # Filter out assets - if not self._validate_pair(tickers[p]): + if not self._validate_pair(p, tickers[p] if p in tickers else {}): pairlist.remove(p) return pairlist diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index db05d5883..c0d2893a1 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -43,19 +43,20 @@ class PrecisionFilter(IPairList): """ return f"{self.name} - Filtering untradable pairs." - def _validate_pair(self, ticker: dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ 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() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ stop_price = ticker['ask'] * self._stoploss # Adjust stop-prices to precision - sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) + sp = self._exchange.price_to_precision(pair, stop_price) - stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) + stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99) logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 3686cd138..20a260b46 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -57,31 +57,32 @@ class PriceFilter(IPairList): return f"{self.name} - No price filters configured." - def _validate_pair(self, ticker) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ 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() :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None or ticker['last'] == 0: - self.log_once(f"Removed {ticker['symbol']} from whitelist, because " + self.log_once(f"Removed {pair} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).", logger.info) return False # Perform low_price_ratio check. if self._low_price_ratio != 0: - compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) + compare = self._exchange.price_get_one_pip(pair, ticker['last']) changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because last price < {self._min_price:.8f}", logger.info) return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 6c4e9f12f..cbbfb9626 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -36,16 +36,17 @@ class SpreadFilter(IPairList): return (f"{self.name} - Filtering pairs with ask/bid diff above " f"{self._max_spread_ratio * 100}%.") - def _validate_pair(self, ticker: dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Validate spread for the ticker + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ if 'bid' in ticker and 'ask' in ticker: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: - self.log_once(f"Removed {ticker['symbol']} from whitelist, because spread " + self.log_once(f"Removed {pair} from whitelist, because spread " f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%", logger.info) return False diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 756368355..f1fecc59c 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -42,7 +42,7 @@ class RangeStabilityFilter(IPairList): If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ - return True + return False def short_desc(self) -> str: """ @@ -51,13 +51,13 @@ class RangeStabilityFilter(IPairList): return (f"{self.name} - Filtering pairs with rate of change below " f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") - def _validate_pair(self, ticker: Dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Validate trading range + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ - pair = ticker['symbol'] # Check symbol in cache if pair in self._pair_cache: return self._pair_cache[pair] From c8dde632272fb2bc83e28a931a345b1c2144586a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 09:05:27 +0100 Subject: [PATCH 194/281] Allow test-pairlist to run with verbosity --- freqtrade/commands/arguments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a7ae969f4..a6c8a245f 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -44,7 +44,8 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] -ARGS_TEST_PAIRLIST = ["config", "quote_currencies", "print_one_column", "list_pairs_print_json"] +ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column", + "list_pairs_print_json"] ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] From 4c0edd0461b694f1065f7541f359f3fd648ebf13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 08:02:53 +0100 Subject: [PATCH 195/281] Move dependencies to base image for RPI --- Dockerfile.armhf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 822da00a5..b6f2e44e6 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -11,10 +11,13 @@ ENV PATH=/root/.local/bin:$PATH RUN mkdir /freqtrade WORKDIR /freqtrade +RUN apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 \ + && apt-get clean + # Install dependencies FROM base as python-deps -RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev libffi-dev libatlas3-base libgfortran5 sqlite3 \ +RUN apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf From 3c85d5201fab2914d63ffc6f2d0cea96f033b91c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 20:38:26 +0100 Subject: [PATCH 196/281] Use async to get candle data for pairlists --- freqtrade/pairlist/AgeFilter.py | 50 ++++++++++------------ freqtrade/pairlist/pairlistmanager.py | 4 +- freqtrade/pairlist/rangestabilityfilter.py | 41 +++++++++++++----- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index f909014ba..21e1b1a01 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -2,7 +2,7 @@ Minimum age (days listed) pair list filter """ import logging -from typing import Any, Dict +from typing import Any, Dict, List import arrow @@ -49,36 +49,32 @@ class AgeFilter(IPairList): return (f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") - def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ - Validate age for the ticker - :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, false if it should be removed + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist """ - - # Check symbol in cache - if ticker['symbol'] in self._symbolsChecked: - return True + needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked] + if not needed_pairs: + return pairlist since_ms = int(arrow.utcnow() .floor('day') - .shift(days=-self._min_days_listed) + .shift(days=-self._min_days_listed - 1) .float_timestamp) * 1000 + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) + pairlist_new = [] + if self._enabled: + for p, _ in needed_pairs: - daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'], - timeframe='1d', - since_ms=since_ms) - - if daily_candles is not None: - 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[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 - return True - else: - self.log_once(f"Removed {ticker['symbol']} from whitelist, because age " - f"{len(daily_candles)} is less than {self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}", logger.info) - return False - return False + age = len(candles[(p, '1d')]) if (p, '1d') in candles else 0 + if age > self._min_days_listed: + pairlist_new.append(p) + self._symbolsChecked[p] = int(arrow.utcnow().float_timestamp) * 1000 + else: + self.log_once(f"Removed {p} from whitelist, because age " + f"{age} is less than {self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}", logger.info) + logger.info(f"Validated {len(pairlist_new)} pairs.") + return pairlist_new diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 810a22300..418cc9e92 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -3,7 +3,7 @@ PairList manager class """ import logging from copy import deepcopy -from typing import Dict, List +from typing import Any, Dict, List from cachetools import TTLCache, cached @@ -97,7 +97,7 @@ class PairListManager(): self._whitelist = pairlist - def _prepare_whitelist(self, pairlist: List[str], tickers) -> List[str]: + def _prepare_whitelist(self, pairlist: List[str], tickers: Dict[str, Any]) -> List[str]: """ Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove pairs that do not have ticker available diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index f1fecc59c..8efded9ee 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -1,8 +1,9 @@ """ Rate of change pairlist filter """ +from copy import deepcopy import logging -from typing import Any, Dict +from typing import Any, Dict, List import arrow from cachetools.ttl import TTLCache @@ -51,7 +52,33 @@ class RangeStabilityFilter(IPairList): return (f"{self.name} - Filtering pairs with rate of change below " f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") - def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Dict[str, Any]) -> bool: """ Validate trading range :param pair: Pair that's currently validated @@ -62,14 +89,6 @@ class RangeStabilityFilter(IPairList): if pair in self._pair_cache: return self._pair_cache[pair] - since_ms = int(arrow.utcnow() - .floor('day') - .shift(days=-self._days) - .float_timestamp) * 1000 - - daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair, - timeframe='1d', - since_ms=since_ms) result = False if daily_candles is not None and not daily_candles.empty: highest_high = daily_candles['high'].max() @@ -79,7 +98,7 @@ class RangeStabilityFilter(IPairList): result = True else: self.log_once(f"Removed {pair} from whitelist, because rate of change " - f"over {plural(self._days, 'day')} is {pct_change:.3f}, " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " f"which is below the threshold of {self._min_rate_of_change}.", logger.info) result = False From 011ba1d9ae74aeaad21cd3ebf2704acc8fa6b3d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 20:49:46 +0100 Subject: [PATCH 197/281] Adapt tests to use async methods --- tests/conftest.py | 2 +- tests/plugins/test_pairlist.py | 53 ++++++++++++++++++++++---------- tests/rpc/test_rpc_apiserver.py | 11 ++++--- tests/strategy/test_interface.py | 11 ++++--- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5d358f015..965980f7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1084,7 +1084,7 @@ def ohlcv_history_list(): @pytest.fixture def ohlcv_history(ohlcv_history_list): return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC", - fill_missing=True) + fill_missing=True, drop_incomplete=False) @pytest.fixture diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index c2a4a69d7..171f0e037 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -353,11 +353,19 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - ohlcv_history_list, pairlists, base_currency, + ohlcv_history, pairlists, base_currency, whitelist_result, caplog) -> None: whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history, + } + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) if whitelist_result == 'static_in_the_middle': @@ -374,7 +382,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) # Provide for PerformanceFilter's dependency @@ -402,7 +410,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t for pairlist in pairlists: if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ - len(ohlcv_history_list) <= pairlist['min_days_listed']: + len(ohlcv_history) <= pairlist['min_days_listed']: assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' r'.* day.*', caplog) if pairlist['method'] == 'PrecisionFilter' and whitelist_result: @@ -575,8 +583,13 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list): - +def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history): + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + } mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), @@ -584,18 +597,18 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) - assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() - assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() # Should not have increased since first call. - assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): @@ -625,7 +638,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): (0.01, 5), (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. ]) -def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, +def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history, min_rate_of_change, expected_length): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'RangeStabilityFilter', 'lookback_days': 2, @@ -636,22 +649,30 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh exchange_has=MagicMock(return_value=True), get_tickers=tickers ) + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history, + ('BLK/BTC', '1d'): ohlcv_history, + } mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == expected_length - assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == expected_length # Should not have increased since first call. - assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 137727e8f..a1f4f7c9d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -870,7 +870,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): def test_api_pair_candles(botclient, ohlcv_history): ftbot, client = botclient timeframe = '5m' - amount = 2 + amount = 3 # No pair rc = client_get(client, @@ -910,8 +910,8 @@ def test_api_pair_candles(botclient, ohlcv_history): assert 'data_stop_ts' in rc.json assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' assert rc.json['data_start_ts'] == 1511686200000 - assert rc.json['data_stop'] == '2017-11-26 08:55:00+00:00' - assert rc.json['data_stop_ts'] == 1511686500000 + assert rc.json['data_stop'] == '2017-11-26 09:00:00+00:00' + assert rc.json['data_stop_ts'] == 1511686800000 assert isinstance(rc.json['columns'], list) assert rc.json['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma', 'buy', 'sell', @@ -926,7 +926,10 @@ def test_api_pair_candles(botclient, ohlcv_history): [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None, 0, 0, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None] + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None], + ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, + 0.7039405, 8.885000000000002e-05, 0, 0, 1511686800000, None, None] + ]) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7cf9a0624..7d6a564b1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -128,27 +128,28 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): def test_assert_df(default_conf, mocker, ohlcv_history, caplog): + df_len = len(ohlcv_history) - 1 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1, - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last close price\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date']) + ohlcv_history.loc[df_len, 'close'] + 0.01, ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last date\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True caplog.clear() _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) + ohlcv_history.loc[2, 'close'], ohlcv_history.loc[0, 'date']) assert log_has_re(r"Dataframe returned from strategy.*last date\.", caplog) # reset to avoid problems in other tests due to test leakage _STRATEGY.disable_dataframe_checks = False From d1fda28d2ea91405a808c90f315ef3e1d0bbd212 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 20:59:58 +0100 Subject: [PATCH 198/281] Fix typehints --- freqtrade/exchange/exchange.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 7 ++++--- tests/strategy/test_interface.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b61049c4e..46c45b5e2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -735,7 +735,7 @@ class Exchange: def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, since_ms: Optional[int] = None, cache: bool = True - ) -> Dict[str, DataFrame]: + ) -> Dict[Tuple[str, str], DataFrame]: """ Refresh in-memory OHLCV asynchronously and set `_klines` with the result Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 8efded9ee..6efe1e2ae 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -1,12 +1,13 @@ """ Rate of change pairlist filter """ -from copy import deepcopy import logging -from typing import Any, Dict, List +from copy import deepcopy +from typing import Any, Dict, List, Optional import arrow from cachetools.ttl import TTLCache +from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural @@ -78,7 +79,7 @@ class RangeStabilityFilter(IPairList): pairlist.remove(p) return pairlist - def _validate_pair_loc(self, pair: str, daily_candles: Dict[str, Any]) -> bool: + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: """ Validate trading range :param pair: Pair that's currently validated diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7d6a564b1..640849ba4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -140,7 +140,8 @@ def test_assert_df(default_conf, mocker, ohlcv_history, caplog): with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last close price\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[df_len, 'close'] + 0.01, ohlcv_history.loc[df_len, 'date']) + ohlcv_history.loc[df_len, 'close'] + 0.01, + ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last date\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), From 266031a6beebf4c27a33e63d0086400aa94bb485 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Dec 2020 19:24:47 +0100 Subject: [PATCH 199/281] Disallow PerformanceFilter for backtesting closes #4072 --- docs/includes/pairlists.md | 6 ++++++ freqtrade/optimize/backtesting.py | 2 ++ tests/optimize/test_backtesting.py | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 844f1d70a..732dfa5bb 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -65,6 +65,9 @@ The `refresh_period` setting allows to define the period (in seconds), at which }], ``` +!!! Note + `VolumePairList` does not support backtesting mode. + #### AgeFilter Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). @@ -84,6 +87,9 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +!!! Note + `PerformanceFilter` does not support backtesting mode. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index de9c52dad..639904975 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -102,6 +102,8 @@ class Backtesting: self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting.") + if 'PerformanceFilter' in self.pairlists.name_list: + raise OperationalException("PerformanceFilter not allowed for backtesting.") if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list: raise OperationalException( diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 547e55db8..971f8d048 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -430,6 +430,11 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): Backtesting(default_conf) + default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] + with pytest.raises(OperationalException, + match='PerformanceFilter not allowed for backtesting.'): + Backtesting(default_conf) + default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ] Backtesting(default_conf) From 4e7f914e92f5e52a450e8ab547ef0d2db057eda5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Dec 2020 13:32:19 +0100 Subject: [PATCH 200/281] Improve test for AgeFilter, fix bug in Agefilter --- freqtrade/pairlist/AgeFilter.py | 45 +++++++++++++++++++++++---------- tests/plugins/test_pairlist.py | 8 +++--- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 21e1b1a01..cc61fd4c3 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -2,9 +2,11 @@ Minimum age (days listed) pair list filter """ import logging -from typing import Any, Dict, List +from copy import deepcopy +from typing import Any, Dict, List, Optional import arrow +from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural @@ -64,17 +66,34 @@ class AgeFilter(IPairList): .shift(days=-self._min_days_listed - 1) .float_timestamp) * 1000 candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) - pairlist_new = [] if self._enabled: - for p, _ in needed_pairs: + for p in deepcopy(pairlist): + 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.") + return pairlist - age = len(candles[(p, '1d')]) if (p, '1d') in candles else 0 - if age > self._min_days_listed: - pairlist_new.append(p) - self._symbolsChecked[p] = int(arrow.utcnow().float_timestamp) * 1000 - else: - self.log_once(f"Removed {p} from whitelist, because age " - f"{age} is less than {self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}", logger.info) - logger.info(f"Validated {len(pairlist_new)} pairs.") - return pairlist_new + 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() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._symbolsChecked: + return True + + if daily_candles is not None: + 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 + return True + else: + self.log_once(f"Removed {pair} from whitelist, because age " + f"{len(daily_candles)} is less than {self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}", logger.info) + return False + return False diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 171f0e037..c4b370e15 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -588,7 +588,6 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, ('LTC/BTC', '1d'): ohlcv_history, - ('XRP/BTC', '1d'): ohlcv_history, } mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -603,12 +602,15 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 + # freqtrade.config['exchange']['pair_whitelist'].append('HOT/BTC') previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() - # Should not have increased since first call. - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + assert len(freqtrade.pairlists.whitelist) == 3 + # Called once for XRP/BTC + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): From ca9fd089918c9c584735af803edef8785460634f Mon Sep 17 00:00:00 2001 From: bigchakalaka <34461529+bigchakalaka@users.noreply.github.com> Date: Thu, 17 Dec 2020 21:40:54 +0100 Subject: [PATCH 201/281] Update strategy-customization.md --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index db007985f..ab64d3a67 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -147,7 +147,7 @@ Let's try to backtest 1 month (January 2019) of 5m candles using an example stra freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m ``` -Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. +Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2018-12-31 15:30:00. If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting. !!! Note From 7d2395ddb7ada3c4e99e5de868cf143303771dd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:44:50 +0100 Subject: [PATCH 202/281] Add limit parameter to fetch_ohlcv --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/exchange.py | 3 ++- freqtrade/exchange/kraken.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 099f282a2..26ec30a8a 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -18,6 +18,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "order_time_in_force": ['gtc', 'fok', 'ioc'], + "ohlcv_candle_limit": 1000, "trades_pagination": "id", "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 46c45b5e2..6f495e605 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -807,7 +807,8 @@ class Exchange: ) data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, - since=since_ms) + since=since_ms, + limit=self._ohlcv_candle_limit) # 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) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 4e4713052..6dbb751e5 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -18,6 +18,7 @@ class Kraken(Exchange): _params: Dict = {"trading_agreement": "agree"} _ft_has: Dict = { "stoploss_on_exchange": True, + "ohlcv_candle_limit": 720, "trades_pagination": "id", "trades_pagination_arg": "since", } From bd0af1b300949c40f9b7c84c14af8ec55e6812f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 19:38:12 +0100 Subject: [PATCH 203/281] Fix test warning --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d8a846124..a42ff52e4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2153,7 +2153,7 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, 'bittrex') + exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) From d7daa86434e5647f27f633e4dcc963f95605d739 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 19:59:46 +0100 Subject: [PATCH 204/281] Add bybit subclass --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bybit.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 freqtrade/exchange/bybit.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 5b58d7a95..15ba7b9f6 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange 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.exchange import (available_exchanges, ccxt_exchanges, get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py new file mode 100644 index 000000000..4a44bb42d --- /dev/null +++ b/freqtrade/exchange/bybit.py @@ -0,0 +1,24 @@ +""" Bybit exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bybit(Exchange): + """ + Bybit exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + # fetchCurrencies API point requires authentication for Bybit, + _ft_has: Dict = { + "ohlcv_candle_limit": 200, + } From 8d3f096a9758c46437c28ed3458546da2be93729 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 20:08:54 +0100 Subject: [PATCH 205/281] AgeFilter does not require tickers --- freqtrade/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index cc61fd4c3..e3465bd82 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -42,7 +42,7 @@ class AgeFilter(IPairList): If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ - return True + return False def short_desc(self) -> str: """ From e92bcb00f6ead312525608dea96c758273ef8107 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 13:43:50 +0100 Subject: [PATCH 206/281] telegram: specify custom shortcut bottons (keyboard) in config.json --- freqtrade/rpc/telegram.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1d36b7e4d..ea9a3c31d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +from itertools import chain from datetime import timedelta from typing import Any, Callable, Dict, List, Union @@ -862,12 +863,39 @@ class Telegram(RPC): :return: None """ + # default / fallback shortcut buttons keyboard: List[List[Union[str, KeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] ] + # do not allow commands with mandatory arguments and critical cmds + # like /forcesell and /forcebuy + valid_btns: List[str] = ['/start', '/stop', '/status', '/status table', + '/trades', '/profit', '/performance', '/daily', + '/stats', '/count', '/locks', '/balance', + '/stopbuy', '/reload_config', '/show_config', + '/logs', '/whitelist', '/blacklist', '/edge', + '/help', '/version'] + # custom shortcuts specified in config.json + shortcut_btns = self._config['telegram'].get('shortcut_btns', []) + if shortcut_btns: + # check for valid shortcuts + invalid_shortcut_btns = [b for b in chain.from_iterable(shortcut_btns) + if b not in valid_btns] + if len(invalid_shortcut_btns): + logger.warning('rpc.telegram: invalid shortcut_btns %s', + invalid_shortcut_btns) + logger.info('rpc.telegram: using default shortcut_btns %s', + keyboard) + else: + keyboard = shortcut_btns + logger.info( + 'rpc.telegram uses custom shortcut bottons specified in ' + + 'config.json %s', [btn for btn in keyboard] + ) + reply_markup = ReplyKeyboardMarkup(keyboard) try: From 5e6897b278a4b367993e08c79ed5562a40de21ae Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 14:48:49 +0100 Subject: [PATCH 207/281] documentation for custom keyboard --- docs/telegram-usage.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c940f59ac..11eb831ad 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -87,6 +87,35 @@ Example configuration showing the different settings: }, ``` +## Create a custom keyboard (command shortcuts buttons) +Telegram allows us to create a custom keyboard with buttons for commands. +The default custom keyboard looks like this. +```python +[ + ['/daily', '/profit', '/balance'], # row 1, 3 commands + ['/status', '/status table', '/performance'], # row 2, 3 commands + ['/count', '/start', '/stop', '/help'] # row 3, 4 commands +] +``` +### Usage +You can create your own keyboard in `config.json`: +``` json +"telegram": { + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id", + "shortcut_btns": [ + ["/daily", "/stats", "/balance", "/profit"], + ["/status table", "/performance"], + ["/reload_config", "/count", "/logs"] + ] + }, +``` +!! NOTE: Only a certain list of commands are allowed. Command arguments are not +supported! +### Supported Commands + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + ## Telegram commands Per default, the Telegram bot shows predefined commands. Some commands From 5b3ffd514138cef8f6c3160f13bdcbc31d8b73d4 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 15:23:40 +0100 Subject: [PATCH 208/281] better log msg, comments --- freqtrade/rpc/telegram.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ea9a3c31d..ec99e4aa9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -872,6 +872,9 @@ class Telegram(RPC): # do not allow commands with mandatory arguments and critical cmds # like /forcesell and /forcebuy + # TODO: DRY! - its not good to list all valid cmds here. But this + # needs refacoring of the whole telegram module (same problem + # in _help()). valid_btns: List[str] = ['/start', '/stop', '/status', '/status table', '/trades', '/profit', '/performance', '/daily', '/stats', '/count', '/locks', '/balance', @@ -885,16 +888,13 @@ class Telegram(RPC): invalid_shortcut_btns = [b for b in chain.from_iterable(shortcut_btns) if b not in valid_btns] if len(invalid_shortcut_btns): - logger.warning('rpc.telegram: invalid shortcut_btns %s', - invalid_shortcut_btns) - logger.info('rpc.telegram: using default shortcut_btns %s', - keyboard) + logger.warning('rpc.telegram: invalid commands for custom ' + f'keyboard: {invalid_shortcut_btns}') + logger.info('rpc.telegram: using default keyboard.') else: keyboard = shortcut_btns - logger.info( - 'rpc.telegram uses custom shortcut bottons specified in ' + - 'config.json %s', [btn for btn in keyboard] - ) + logger.info('rpc.telegram using custom keyboard from ' + f'config.json: {[btn for btn in keyboard]}') reply_markup = ReplyKeyboardMarkup(keyboard) From bf920994868a30113c57b665e7739261f4a91ac8 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 16:35:54 +0100 Subject: [PATCH 209/281] test for custom keyboard --- tests/rpc/test_rpc_telegram.py | 48 +++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ecad05683..62c60d8e6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -10,7 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -from telegram import Chat, Message, Update +from telegram import Chat, Message, Update, ReplyKeyboardMarkup from telegram.error import NetworkError from freqtrade import __version__ @@ -1729,3 +1729,49 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: # Bot should've tried to send it twice assert len(bot.method_calls) == 2 assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog) + + +def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + bot = MagicMock() + bot.send_message = MagicMock() + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot + + invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']] + default_keys_list = [['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help']] + default_keyboard = ReplyKeyboardMarkup(default_keys_list) + + custom_keys_list = [['/daily', '/stats', '/balance', '/profit'], + ['/count', '/start', '/reload_config', '/help']] + custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) + + # no shortcut_btns in config -> default keyboard + telegram._config['telegram']['enabled'] = True + telegram._send_msg('test') + used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + assert used_keyboard == default_keyboard + + # invalid shortcut_btns in config -> default keyboard + telegram._config['telegram']['enabled'] = True + telegram._config['telegram']['shortcut_btns'] = invalid_keys_list + telegram._send_msg('test') + used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + assert used_keyboard == default_keyboard + assert log_has("rpc.telegram: invalid commands for custom keyboard: " + "['/not_valid', '/alsoinvalid']", caplog) + assert log_has('rpc.telegram: using default keyboard.', caplog) + + # valid shortcut_btns in config -> custom keyboard + telegram._config['telegram']['enabled'] = True + telegram._config['telegram']['shortcut_btns'] = custom_keys_list + telegram._send_msg('test') + used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + assert used_keyboard == custom_keyboard + assert log_has("rpc.telegram using custom keyboard from config.json: " + "[['/daily', '/stats', '/balance', '/profit'], ['/count', " + "'/start', '/reload_config', '/help']]", caplog) From 621105df9aecdc7a278cc89f035a7521a0fb1e23 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 16:50:42 +0100 Subject: [PATCH 210/281] renaming shortcut_btns to keyboard --- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/telegram.py | 14 +++++++------- tests/rpc/test_rpc_telegram.py | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 11eb831ad..950b4df1e 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -87,7 +87,7 @@ Example configuration showing the different settings: }, ``` -## Create a custom keyboard (command shortcuts buttons) +## Create a custom keyboard (command shortcut buttons) Telegram allows us to create a custom keyboard with buttons for commands. The default custom keyboard looks like this. ```python @@ -104,7 +104,7 @@ You can create your own keyboard in `config.json`: "enabled": true, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id", - "shortcut_btns": [ + "keyboard": [ ["/daily", "/stats", "/balance", "/profit"], ["/status table", "/performance"], ["/reload_config", "/count", "/logs"] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ec99e4aa9..3260d0d4f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -882,17 +882,17 @@ class Telegram(RPC): '/logs', '/whitelist', '/blacklist', '/edge', '/help', '/version'] # custom shortcuts specified in config.json - shortcut_btns = self._config['telegram'].get('shortcut_btns', []) - if shortcut_btns: + cust_keyboard = self._config['telegram'].get('keyboard', []) + if cust_keyboard: # check for valid shortcuts - invalid_shortcut_btns = [b for b in chain.from_iterable(shortcut_btns) - if b not in valid_btns] - if len(invalid_shortcut_btns): + invalid_keys = [b for b in chain.from_iterable(cust_keyboard) + if b not in valid_btns] + if len(invalid_keys): logger.warning('rpc.telegram: invalid commands for custom ' - f'keyboard: {invalid_shortcut_btns}') + f'keyboard: {invalid_keys}') logger.info('rpc.telegram: using default keyboard.') else: - keyboard = shortcut_btns + keyboard = cust_keyboard logger.info('rpc.telegram using custom keyboard from ' f'config.json: {[btn for btn in keyboard]}') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 62c60d8e6..5bbc68639 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1750,15 +1750,15 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: ['/count', '/start', '/reload_config', '/help']] custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) - # no shortcut_btns in config -> default keyboard + # no keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True telegram._send_msg('test') used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] assert used_keyboard == default_keyboard - # invalid shortcut_btns in config -> default keyboard + # invalid keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['shortcut_btns'] = invalid_keys_list + telegram._config['telegram']['keyboard'] = invalid_keys_list telegram._send_msg('test') used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] assert used_keyboard == default_keyboard @@ -1766,9 +1766,9 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: "['/not_valid', '/alsoinvalid']", caplog) assert log_has('rpc.telegram: using default keyboard.', caplog) - # valid shortcut_btns in config -> custom keyboard + # valid keyboard in config -> custom keyboard telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['shortcut_btns'] = custom_keys_list + telegram._config['telegram']['keyboard'] = custom_keys_list telegram._send_msg('test') used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] assert used_keyboard == custom_keyboard From 799e6be2eba9f0b385d3bed1d2f9665743f37029 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 17:13:03 +0100 Subject: [PATCH 211/281] fix tests --- tests/rpc/test_rpc_telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5bbc68639..5a88edf54 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1753,14 +1753,14 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # no keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True telegram._send_msg('test') - used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard # invalid keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True telegram._config['telegram']['keyboard'] = invalid_keys_list telegram._send_msg('test') - used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard assert log_has("rpc.telegram: invalid commands for custom keyboard: " "['/not_valid', '/alsoinvalid']", caplog) @@ -1770,7 +1770,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: telegram._config['telegram']['enabled'] = True telegram._config['telegram']['keyboard'] = custom_keys_list telegram._send_msg('test') - used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard assert log_has("rpc.telegram using custom keyboard from config.json: " "[['/daily', '/stats', '/balance', '/profit'], ['/count', " From 6b44545d37bd197e53c56fdc99d0965c14608553 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 17:22:23 +0100 Subject: [PATCH 212/281] sort order imports --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3260d0d4f..466c58878 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,8 +5,8 @@ This module manage Telegram communication """ import json import logging -from itertools import chain from datetime import timedelta +from itertools import chain from typing import Any, Callable, Dict, List, Union import arrow diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5a88edf54..ce3db7130 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -10,7 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -from telegram import Chat, Message, Update, ReplyKeyboardMarkup +from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import NetworkError from freqtrade import __version__ From ecadfdd98ea7fcb22265d37a67a6494706047e8a Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 17 May 2020 12:24:04 +0200 Subject: [PATCH 213/281] fixed:advanced config. added. feature: fill area between traces by advanced configuration. --- freqtrade/plot/plotting.py | 40 +++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f7d300593..ec0d53a0b 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -236,12 +236,20 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :param plot_config: Dict of Dicts containing advanced plot configuration :return: plot_config - eventually with indicators 1 and 2 """ - if plot_config: if indicators1: - plot_config['main_plot'] = {ind: {} for ind in indicators1} + for ind in indicators1: + #add indicators with no advanced plot_config. + if ind not in plot_config['main_plot'].keys(): + plot_config['main_plot'][ind] = {} if indicators2: - plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}} + #'Other' key not provided in strategy.plot_config. + if 'Other' not in plot_config['subplots'].keys(): + plot_config['subplots'] = {'Other': plot_config['subplots']} + for ind in indicators2: + #add indicators with no advanced plot_config + if ind not in plot_config['subplots']['Other'].keys(): + plot_config['subplots']['Other'][ind] = {} if not plot_config: # If no indicators and no plot-config given, use defaults. @@ -255,6 +263,8 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], 'main_plot': {ind: {} for ind in indicators1}, 'subplots': {'Other': {ind: {} for ind in indicators2}}, } + + #!!!NON SENSE - isnt it? if 'main_plot' not in plot_config: plot_config['main_plot'] = {} @@ -280,6 +290,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) + print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] @@ -370,6 +381,29 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra del plot_config['main_plot']['bb_upperband'] del plot_config['main_plot']['bb_lowerband'] + #fill area betwenn traces i.e. for ichimoku + if 'fill_area' in plot_config.keys(): + for area in plot_config['fill_area']: + #!error: need exactly 2 trace + traces = area['traces'] + color = area['color'] + if traces[0] in data and traces[1] in data: + trace_b = go.Scatter( + x=data.date, + y=data.get(traces[0]), + showlegend=False, + line={'color': 'rgba(255,255,255,0)'}, + ) + trace_a = go.Scatter( + x=data.date, + y=data.get(traces[1]), + name=f'{traces[0]} * {traces[1]}', + fill="tonexty", + fillcolor=color, + line={'color': 'rgba(255,255,255,0)'}, + ) + fig.add_trace(trace_b) + fig.add_trace(trace_a) # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) From 3fdfc06a1e6046d61bb420d7b52d2dd8d6427e4b Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 17 May 2020 12:43:38 +0200 Subject: [PATCH 214/281] label for fill_area added and documentation updated --- docs/plotting.md | 9 ++++++++- freqtrade/plot/plotting.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 09eb6ddb5..f794cdedc 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -168,6 +168,7 @@ Additional features when using plot_config include: * Specify colors per indicator * Specify additional subplots +* Specify idicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. @@ -194,7 +195,13 @@ Sample configuration with inline comments explaining the process: "RSI": { 'rsi': {'color': 'red'}, } - } + }, + 'fill_area': { + "Ichimoku Cloud": { + 'traces': ('senkou_a', 'senkou_b'), + 'color': 'rgba(0,176,246,0.2)', + }, + } } ``` diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ec0d53a0b..2d0d01388 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -383,7 +383,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra #fill area betwenn traces i.e. for ichimoku if 'fill_area' in plot_config.keys(): - for area in plot_config['fill_area']: + for label, area in plot_config['fill_area'].items(): #!error: need exactly 2 trace traces = area['traces'] color = area['color'] @@ -397,7 +397,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra trace_a = go.Scatter( x=data.date, y=data.get(traces[1]), - name=f'{traces[0]} * {traces[1]}', + name=label, fill="tonexty", fillcolor=color, line={'color': 'rgba(255,255,255,0)'}, From daa1727e2bfd641058728eb8161d615e2db1c66a Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 17 May 2020 13:13:32 +0200 Subject: [PATCH 215/281] Exeption for fill_area.traces --- freqtrade/plot/plotting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 2d0d01388..aed752b94 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -290,7 +290,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] @@ -384,8 +383,13 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra #fill area betwenn traces i.e. for ichimoku if 'fill_area' in plot_config.keys(): for label, area in plot_config['fill_area'].items(): - #!error: need exactly 2 trace traces = area['traces'] + if len(traces) != 2: + raise Exception( + f"plot_config.fill_area.traces = {traces}: " \ + + f"needs exactly 2 indicators. " \ + + f"{len(traces)} is given." + ) color = area['color'] if traces[0] in data and traces[1] in data: trace_b = go.Scatter( From fdd4b40c3468d6cf8145202eeb058c6c36a50855 Mon Sep 17 00:00:00 2001 From: Christof Date: Thu, 21 May 2020 08:28:58 +0200 Subject: [PATCH 216/281] fixed subplots, empty create plot_config if its not given by strategie --- freqtrade/plot/plotting.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index aed752b94..ea18e7102 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -237,19 +237,24 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :return: plot_config - eventually with indicators 1 and 2 """ if plot_config: + #maybe main or sub is given, not both. + if 'main_plot' not in plot_config.keys(): + plot_config['main_plot'] = {} + + if 'subplots' not in plot_config.keys(): + plot_config['subplots'] = {} if indicators1: for ind in indicators1: - #add indicators with no advanced plot_config. + #add indicators with NO advanced plot_config, only! to be sure + #indicator colors given in advanced plot_config will not be + #overwritten. if ind not in plot_config['main_plot'].keys(): plot_config['main_plot'][ind] = {} if indicators2: - #'Other' key not provided in strategy.plot_config. - if 'Other' not in plot_config['subplots'].keys(): - plot_config['subplots'] = {'Other': plot_config['subplots']} - for ind in indicators2: - #add indicators with no advanced plot_config - if ind not in plot_config['subplots']['Other'].keys(): - plot_config['subplots']['Other'][ind] = {} + #add other indicators given on cmd line to advanced plot_config. + plot_config['subplots'].update( + {'Other' : {ind : {} for ind in indicators2}} + ) if not plot_config: # If no indicators and no plot-config given, use defaults. @@ -264,12 +269,6 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], 'subplots': {'Other': {ind: {} for ind in indicators2}}, } - #!!!NON SENSE - isnt it? - if 'main_plot' not in plot_config: - plot_config['main_plot'] = {} - - if 'subplots' not in plot_config: - plot_config['subplots'] = {} return plot_config @@ -290,7 +289,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - + print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] # Define the graph From fb3d82ccb9221a8928a9a81618bfade526853820 Mon Sep 17 00:00:00 2001 From: Christof Date: Thu, 21 May 2020 08:32:12 +0200 Subject: [PATCH 217/281] cleanup --- freqtrade/plot/plotting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ea18e7102..02636d831 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -289,7 +289,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] # Define the graph From 4531c924da55ecab7eb6a704d695a84c33e0aa19 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 25 May 2020 09:05:24 +0200 Subject: [PATCH 218/281] PEP8 --- freqtrade/plot/plotting.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 02636d831..bed0319a6 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -237,7 +237,7 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :return: plot_config - eventually with indicators 1 and 2 """ if plot_config: - #maybe main or sub is given, not both. + # maybe main or sub is given, not both. if 'main_plot' not in plot_config.keys(): plot_config['main_plot'] = {} @@ -245,15 +245,15 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config['subplots'] = {} if indicators1: for ind in indicators1: - #add indicators with NO advanced plot_config, only! to be sure - #indicator colors given in advanced plot_config will not be - #overwritten. + # add indicators with NO advanced plot_config, only! to be sure + # indicator colors given in advanced plot_config will not be + # overwritten. if ind not in plot_config['main_plot'].keys(): plot_config['main_plot'][ind] = {} if indicators2: - #add other indicators given on cmd line to advanced plot_config. + # add other indicators given on cmd line to advanced plot_config. plot_config['subplots'].update( - {'Other' : {ind : {} for ind in indicators2}} + {'Other': {ind: {} for ind in indicators2}} ) if not plot_config: @@ -378,15 +378,15 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra del plot_config['main_plot']['bb_upperband'] del plot_config['main_plot']['bb_lowerband'] - #fill area betwenn traces i.e. for ichimoku + # fill area betwenn traces i.e. for ichimoku if 'fill_area' in plot_config.keys(): for label, area in plot_config['fill_area'].items(): traces = area['traces'] if len(traces) != 2: raise Exception( - f"plot_config.fill_area.traces = {traces}: " \ - + f"needs exactly 2 indicators. " \ - + f"{len(traces)} is given." + f"plot_config.fill_area.traces = {traces}: " + + f"needs exactly 2 indicators. " + + f"{len(traces)} is given." ) color = area['color'] if traces[0] in data and traces[1] in data: @@ -406,6 +406,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(trace_b) fig.add_trace(trace_a) + # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) From cc39cf97ddee25768ed26f6b8b8afc8f8fd52857 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 14:14:20 +0100 Subject: [PATCH 219/281] revert to former create_plotconfig behaviour --- freqtrade/plot/plotting.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index bed0319a6..7aa332501 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -226,7 +226,6 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: logger.warning("No trades found.") return fig - def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict]) -> Dict[str, Dict]: """ @@ -236,25 +235,12 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :param plot_config: Dict of Dicts containing advanced plot configuration :return: plot_config - eventually with indicators 1 and 2 """ - if plot_config: - # maybe main or sub is given, not both. - if 'main_plot' not in plot_config.keys(): - plot_config['main_plot'] = {} - if 'subplots' not in plot_config.keys(): - plot_config['subplots'] = {} + if plot_config: if indicators1: - for ind in indicators1: - # add indicators with NO advanced plot_config, only! to be sure - # indicator colors given in advanced plot_config will not be - # overwritten. - if ind not in plot_config['main_plot'].keys(): - plot_config['main_plot'][ind] = {} + plot_config['main_plot'] = {ind: {} for ind in indicators1} if indicators2: - # add other indicators given on cmd line to advanced plot_config. - plot_config['subplots'].update( - {'Other': {ind: {} for ind in indicators2}} - ) + plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}} if not plot_config: # If no indicators and no plot-config given, use defaults. @@ -268,10 +254,13 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], 'main_plot': {ind: {} for ind in indicators1}, 'subplots': {'Other': {ind: {} for ind in indicators2}}, } + if 'main_plot' not in plot_config: + plot_config['main_plot'] = {} + if 'subplots' not in plot_config: + plot_config['subplots'] = {} return plot_config - def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], From 75e4758936ac99d3fc9f756730d6f678480c8035 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:06:21 +0100 Subject: [PATCH 220/281] changed config params, added fill area in subplots --- freqtrade/plot/plotting.py | 113 +++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7aa332501..62a667e03 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -226,6 +226,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: logger.warning("No trades found.") return fig + def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict]) -> Dict[str, Dict]: """ @@ -261,6 +262,34 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config['subplots'] = {} return plot_config + +def add_filled_traces(fig, row: int, data: pd.DataFrame, indicator_a: str, + indicator_b: str, label: str = "", + fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: + """ Adds plots for two traces, which are filled between to fig. + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param indicator_a: indicator name as populated in stragetie + :param indicator_b: indicator name as populated in stragetie + :param label: label for the filled area + :param fill_color: color to be used for the filled area + :return: fig with added filled_traces plot + """ + if indicator_a in data and indicator_b in data: + # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 + trace_a = go.Scatter(x=data.date, y=data[indicator_a], + showlegend=False, + line={'color': 'rgba(255,255,255,0)'}) + + trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, + fill="tonexty", fillcolor=fill_color, + line={'color': 'rgba(255,255,255,0)'}) + fig.add_trace(trace_a, row, 1) + fig.add_trace(trace_b, row, 1) + return fig + + def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -344,61 +373,28 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra else: logger.warning("No sell-signals found.") - # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 - if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( - x=data.date, - y=data.bb_lowerband, - showlegend=False, - line={'color': 'rgba(255,255,255,0)'}, - ) - bb_upper = go.Scatter( - x=data.date, - y=data.bb_upperband, - name='Bollinger Band', - fill="tonexty", - fillcolor="rgba(0,176,246,0.2)", - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.add_trace(bb_lower, 1, 1) - fig.add_trace(bb_upper, 1, 1) - if ('bb_upperband' in plot_config['main_plot'] - and 'bb_lowerband' in plot_config['main_plot']): - del plot_config['main_plot']['bb_upperband'] - del plot_config['main_plot']['bb_lowerband'] - - # fill area betwenn traces i.e. for ichimoku - if 'fill_area' in plot_config.keys(): - for label, area in plot_config['fill_area'].items(): - traces = area['traces'] - if len(traces) != 2: - raise Exception( - f"plot_config.fill_area.traces = {traces}: " + - f"needs exactly 2 indicators. " + - f"{len(traces)} is given." - ) - color = area['color'] - if traces[0] in data and traces[1] in data: - trace_b = go.Scatter( - x=data.date, - y=data.get(traces[0]), - showlegend=False, - line={'color': 'rgba(255,255,255,0)'}, - ) - trace_a = go.Scatter( - x=data.date, - y=data.get(traces[1]), - name=label, - fill="tonexty", - fillcolor=color, - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.add_trace(trace_b) - fig.add_trace(trace_a) + # Add Boilinger Bands + fig = add_filled_traces(fig, 1, data, 'bb_lowerband', 'bb_upperband', + label="Boillinger Band") + # prevent bb_lower and bb_upper from plotting + try: + del plot_config['main_plot']['bb_lowerband'] + del plot_config['main_plot']['bb_upperband'] + except KeyError: + pass # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) + # fill area between indicators ( 'fill_to': 'other_indicator') + for indicator, ind_conf in plot_config['main_plot'].items(): + if 'fill_to' in ind_conf: + label = ind_conf.get('fill_label', '') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = add_filled_traces(fig, 1, data, indicator, + ind_conf['fill_to'], label=label, + fill_color=fill_color) + fig = plot_trades(fig, trades) # Volume goes to row 2 @@ -412,11 +408,20 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fig.add_trace(volume, 2, 1) # Add indicators to separate row - for i, name in enumerate(plot_config['subplots']): - fig = add_indicators(fig=fig, row=3 + i, - indicators=plot_config['subplots'][name], + for i, label in enumerate(plot_config['subplots']): + sub_config = plot_config['subplots'][label] + row = 3 + i + fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data) + # fill area between indicators ( 'fill_to': 'other_indicator') + for indicator, ind_config in sub_config.items(): + if 'fill_to' in ind_config: + label = ind_config.get('fill_label', '') + fill_color = ind_config.get('fill_color', 'rgba(0,176,246,0.2)') + fig = add_filled_traces(fig, row, data, indicator, + ind_config['fill_to'], label=label, + fill_color=fill_color) return fig From d901a86165736df19eae7495f631de7f66b751d3 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:19:32 +0100 Subject: [PATCH 221/281] typo --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 62a667e03..b77b76e1a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -375,7 +375,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra # Add Boilinger Bands fig = add_filled_traces(fig, 1, data, 'bb_lowerband', 'bb_upperband', - label="Boillinger Band") + label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: del plot_config['main_plot']['bb_lowerband'] From 16baca5eeb2bca0ff79746d8dc420b6042428a7a Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:42:22 +0100 Subject: [PATCH 222/281] fixed: too complex warning --- freqtrade/plot/plotting.py | 46 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b77b76e1a..7d17585a1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -263,10 +263,10 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], return plot_config -def add_filled_traces(fig, row: int, data: pd.DataFrame, indicator_a: str, +def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, indicator_b: str, label: str = "", fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Adds plots for two traces, which are filled between to fig. + """ Plots the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -290,6 +290,25 @@ def add_filled_traces(fig, row: int, data: pd.DataFrame, indicator_a: str, return fig +def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: + """ Adds all areas (from plot_config) to fig. + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param indicators: dict with indicators. ie.: plot_config['main_plot'] or + plot_config['subplots'][subplot_label] + :return: fig with added filled_traces plot + """ + for indicator, ind_conf in indicators.items(): + if 'fill_to' in ind_conf: + label = ind_conf.get('fill_label', '') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = plot_area_between(fig, row, data, indicator, + ind_conf['fill_to'], label=label, + fill_color=fill_color) + return fig + + def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -373,8 +392,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra else: logger.warning("No sell-signals found.") - # Add Boilinger Bands - fig = add_filled_traces(fig, 1, data, 'bb_lowerband', 'bb_upperband', + # Add Bollinger Bands + fig = plot_area_between(fig, 1, data, 'bb_lowerband', 'bb_upperband', label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: @@ -385,15 +404,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) - # fill area between indicators ( 'fill_to': 'other_indicator') - for indicator, ind_conf in plot_config['main_plot'].items(): - if 'fill_to' in ind_conf: - label = ind_conf.get('fill_label', '') - fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = add_filled_traces(fig, 1, data, indicator, - ind_conf['fill_to'], label=label, - fill_color=fill_color) + fig = add_areas(fig, 1, data, plot_config['main_plot']) fig = plot_trades(fig, trades) @@ -413,15 +425,9 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra row = 3 + i fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data) - # fill area between indicators ( 'fill_to': 'other_indicator') - for indicator, ind_config in sub_config.items(): - if 'fill_to' in ind_config: - label = ind_config.get('fill_label', '') - fill_color = ind_config.get('fill_color', 'rgba(0,176,246,0.2)') - fig = add_filled_traces(fig, row, data, indicator, - ind_config['fill_to'], label=label, - fill_color=fill_color) + fig = add_areas(fig, row, data, sub_config) + return fig From 5b2902fcbcdd9ff4a3ed9510f2f064becaa0f3e7 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:48:08 +0100 Subject: [PATCH 223/281] cleanup --- freqtrade/plot/plotting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7d17585a1..0f8c99852 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -266,7 +266,7 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, indicator_b: str, label: str = "", fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Plots the area between two traces and adds it to fig. + """ Creates plot for the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -277,21 +277,21 @@ def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, :return: fig with added filled_traces plot """ if indicator_a in data and indicator_b in data: + line = {'color': 'rgba(255,255,255,0)'} # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 trace_a = go.Scatter(x=data.date, y=data[indicator_a], showlegend=False, - line={'color': 'rgba(255,255,255,0)'}) - + line=line) trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, fill="tonexty", fillcolor=fill_color, - line={'color': 'rgba(255,255,255,0)'}) + line=line) fig.add_trace(trace_a, row, 1) fig.add_trace(trace_b, row, 1) return fig def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: - """ Adds all areas (from plot_config) to fig. + """ Adds all area plots (specified in plot_config) to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame From 8b248780231c9b5306a1f42ef0764402f0ffd12e Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 18:21:26 +0100 Subject: [PATCH 224/281] plot_config documentation for fill_to, fill_label, fill_color --- docs/plotting.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index f794cdedc..30844606c 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -168,7 +168,7 @@ Additional features when using plot_config include: * Specify colors per indicator * Specify additional subplots -* Specify idicator pairs to fill area in between +* Specify indicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. @@ -184,29 +184,32 @@ Sample configuration with inline comments explaining the process: 'ema50': {'color': '#CCCCCC'}, # By omitting color, a random color is selected. 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud' #optional, + 'fill_color': 'rgba(255,76,46,0.2)', #optional + }, + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} }, 'subplots': { # Create subplot MACD "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'} }, # Additional subplot RSI "RSI": { - 'rsi': {'color': 'red'}, + 'rsi': {'color': 'red'} } - }, - 'fill_area': { - "Ichimoku Cloud": { - 'traces': ('senkou_a', 'senkou_b'), - 'color': 'rgba(0,176,246,0.2)', - }, - } + } } -``` +``` !!! Note - The above configuration assumes that `ema10`, `ema50`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. + The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. ## Plot profit From 43091a26ce19ce4bdfadf012ff6e9ff83b559101 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:32:13 +0100 Subject: [PATCH 225/281] simple tests --- freqtrade/plot/plotting.py | 21 +++++++++++++++----- tests/test_plotting.py | 40 +++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 0f8c99852..6e92bced8 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -301,11 +301,22 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: """ for indicator, ind_conf in indicators.items(): if 'fill_to' in ind_conf: - label = ind_conf.get('fill_label', '') - fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = plot_area_between(fig, row, data, indicator, - ind_conf['fill_to'], label=label, - fill_color=fill_color) + indicator_b = ind_conf['fill_to'] + if indicator in data and indicator_b in data: + label = ind_conf.get('fill_label', '') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = plot_area_between(fig, row, data, indicator, indicator_b, + label=label, fill_color=fill_color) + elif indicator not in data: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not ' + 'found in your strategy.', indicator + ) + elif indicator_b not in data: + logger.info( + 'fill_to: "%s" ignored. Reason: This indicator is not ' + 'in your strategy.', indicator_b + ) return fig diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d3f97013d..96eff4c69 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -16,7 +16,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, load_and_plot_trades, - plot_profit, plot_trades, store_plot_file) + plot_profit, plot_trades, store_plot_file, + add_areas) from freqtrade.resolvers import StrategyResolver from tests.conftest import get_args, log_has, log_has_re, patch_exchange @@ -96,6 +97,42 @@ def test_add_indicators(default_conf, testdatadir, caplog): assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) +def test_add_areas(default_conf, testdatadir, caplog): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = history.load_pair_history(pair=pair, timeframe='1m', + datadir=testdatadir, timerange=timerange) + indicators = {"macd": {"color": "red", + "fill_color": "black", + "fill_to": "macdhist", + "fill_label": "MACD Fill"}} + + default_conf.update({'strategy': 'DefaultStrategy'}) + strategy = StrategyResolver.load_strategy(default_conf) + + # Generate buy/sell signals and indicators + data = strategy.analyze_ticker(data, {'pair': pair}) + fig = generate_empty_figure() + + # indicator mentioned in fill_to does not exist + fig1 = add_areas(fig, 1, data, {'ema10': {'fill_to': 'no_fill_indicator'}}) + assert fig == fig1 + assert log_has_re(r'fill_to: "no_fill_indicator" ignored\..*', caplog) + + # indicator does not exist + fig2 = add_areas(fig, 1, data, {'no_indicator': {'fill_to': 'ema10'}}) + assert fig == fig2 + assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) + + fig3 = add_areas(fig, 3, data, indicators) + figure = fig3.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y3" + assert fill_macd.fillcolor == "black" + + def test_plot_trades(testdatadir, caplog): fig1 = generate_empty_figure() # nothing happens when no trades are available @@ -136,6 +173,7 @@ def test_plot_trades(testdatadir, caplog): assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min' + def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) From f24626e13920377f0208292a4f5326ccf1eca023 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:42:19 +0100 Subject: [PATCH 226/281] removed too many blank lines --- tests/test_plotting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 96eff4c69..379889ea1 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -173,7 +173,6 @@ def test_plot_trades(testdatadir, caplog): assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min' - def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) From f120c8d6c7c33419b315ebde415f17c1ec242f87 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:47:25 +0100 Subject: [PATCH 227/281] documentation --- docs/plotting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plotting.md b/docs/plotting.md index 30844606c..ed682e44b 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -209,7 +209,8 @@ Sample configuration with inline comments explaining the process: ``` !!! Note - The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. + The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, + `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. ## Plot profit From fabb31e1bc20a0040609ce180787a98f3d83870f Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:50:15 +0100 Subject: [PATCH 228/281] imports order --- tests/test_plotting.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 379889ea1..5bd9f4f9d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -13,11 +13,10 @@ from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.exceptions import OperationalException -from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, +from freqtrade.plot.plotting import (add_areas, add_indicators, add_profit, create_plotconfig, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, load_and_plot_trades, - plot_profit, plot_trades, store_plot_file, - add_areas) + plot_profit, plot_trades, store_plot_file) from freqtrade.resolvers import StrategyResolver from tests.conftest import get_args, log_has, log_has_re, patch_exchange From c1b8ad723261d89157720d9bfe4c77e46c0bfe5f Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 21:37:52 +0100 Subject: [PATCH 229/281] renaming, comments, cleanups --- freqtrade/plot/plotting.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6e92bced8..b4adf4049 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -263,10 +263,10 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], return plot_config -def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, - indicator_b: str, label: str = "", - fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Creates plot for the area between two traces and adds it to fig. +def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str, + indicator_b: str, label: str = "", + fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: + """ Creates a plot for the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -277,6 +277,7 @@ def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, :return: fig with added filled_traces plot """ if indicator_a in data and indicator_b in data: + # make lines invisible to get the area plotted, only. line = {'color': 'rgba(255,255,255,0)'} # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 trace_a = go.Scatter(x=data.date, y=data[indicator_a], @@ -303,10 +304,11 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: if 'fill_to' in ind_conf: indicator_b = ind_conf['fill_to'] if indicator in data and indicator_b in data: - label = ind_conf.get('fill_label', '') + label = ind_conf.get('fill_label', + f'{indicator}<>{indicator_b}') fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = plot_area_between(fig, row, data, indicator, indicator_b, - label=label, fill_color=fill_color) + fig = plot_area(fig, row, data, indicator, indicator_b, + label=label, fill_color=fill_color) elif indicator not in data: logger.info( 'Indicator "%s" ignored. Reason: This indicator is not ' @@ -402,25 +404,20 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fig.add_trace(sells, 1, 1) else: logger.warning("No sell-signals found.") - # Add Bollinger Bands - fig = plot_area_between(fig, 1, data, 'bb_lowerband', 'bb_upperband', - label="Bollinger Band") + fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', + label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: del plot_config['main_plot']['bb_lowerband'] del plot_config['main_plot']['bb_upperband'] except KeyError: pass - - # Add indicators to main plot + # main plot goes to row 1 fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) - # fill area between indicators ( 'fill_to': 'other_indicator') fig = add_areas(fig, 1, data, plot_config['main_plot']) - fig = plot_trades(fig, trades) - - # Volume goes to row 2 + # sub plot: Volume goes to row 2 volume = go.Bar( x=data['date'], y=data['volume'], @@ -429,8 +426,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra marker_line_color='DarkSlateGrey' ) fig.add_trace(volume, 2, 1) - - # Add indicators to separate row + # Add each sub plot to a separate row for i, label in enumerate(plot_config['subplots']): sub_config = plot_config['subplots'][label] row = 3 + i @@ -438,7 +434,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra data=data) # fill area between indicators ( 'fill_to': 'other_indicator') fig = add_areas(fig, row, data, sub_config) - return fig From 3cb559994e108af03239ed4bce3f1769d3cd05b1 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 21:47:11 +0100 Subject: [PATCH 230/281] some more test --- tests/test_plotting.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 5bd9f4f9d..42847ca50 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -107,6 +107,10 @@ def test_add_areas(default_conf, testdatadir, caplog): "fill_to": "macdhist", "fill_label": "MACD Fill"}} + ind_no_label = {"macd": {"fill_color": "red", + "fill_to": "macdhist"}} + + ind_plain = {"macd": {"fill_to": "macdhist"}} default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -124,6 +128,7 @@ def test_add_areas(default_conf, testdatadir, caplog): assert fig == fig2 assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) + # everythin given in plot config, row 3 fig3 = add_areas(fig, 3, data, indicators) figure = fig3.layout.figure fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill") @@ -131,6 +136,21 @@ def test_add_areas(default_conf, testdatadir, caplog): assert fill_macd.yaxis == "y3" assert fill_macd.fillcolor == "black" + # label missing, row 1 + fig4 = add_areas(fig, 1, data, ind_no_label) + figure = fig4.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y" + assert fill_macd.fillcolor == "red" + + # fit_to only + fig5 = add_areas(fig, 1, data, ind_plain) + figure = fig5.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y" + def test_plot_trades(testdatadir, caplog): fig1 = generate_empty_figure() From 18a24d75efcbdc19f162d9c4310ae257a5252a3a Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 22:01:33 +0100 Subject: [PATCH 231/281] cleanup --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b4adf4049..497218deb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -426,7 +426,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra marker_line_color='DarkSlateGrey' ) fig.add_trace(volume, 2, 1) - # Add each sub plot to a separate row + # add each sub plot to a separate row for i, label in enumerate(plot_config['subplots']): sub_config = plot_config['subplots'][label] row = 3 + i From f39dde121ab5d836d1be616d19858c7f35fe1fe2 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 22:36:56 +0100 Subject: [PATCH 232/281] moved keyboard config validation to __inti__ --- freqtrade/rpc/telegram.py | 79 ++++++++++++++++++---------------- tests/rpc/test_rpc_telegram.py | 14 ++++-- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 466c58878..1ba45e089 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -7,11 +7,11 @@ import json import logging from datetime import timedelta from itertools import chain -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update +from telegram import ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -75,10 +75,48 @@ class Telegram(RPC): self._updater: Updater self._config = freqtrade.config + self._validate_keyboard() self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() + def _validate_keyboard(self) -> None: + """ + Validates the keyboard configuration from telegram config + section. + """ + self._keyboard: List[List[str]] = [ + ['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help'] + ] + # do not allow commands with mandatory arguments and critical cmds + # like /forcesell and /forcebuy + # TODO: DRY! - its not good to list all valid cmds here. But otherwise + # this needs refacoring of the whole telegram module (same + # problem in _help()). + valid_keys: List[str] = ['/start', '/stop', '/status', '/status table', + '/trades', '/profit', '/performance', '/daily', + '/stats', '/count', '/locks', '/balance', + '/stopbuy', '/reload_config', '/show_config', + '/logs', '/whitelist', '/blacklist', '/edge', + '/help', '/version'] + + # custom shortcuts specified in config.json + cust_keyboard = self._config['telegram'].get('keyboard', []) + if cust_keyboard: + # check for valid shortcuts + invalid_keys = [b for b in chain.from_iterable(cust_keyboard) + if b not in valid_keys] + if len(invalid_keys): + logger.warning('rpc.telegram: invalid commands for custom ' + f'keyboard: {invalid_keys}') + logger.info('rpc.telegram: using default keyboard.') + else: + self._keyboard = cust_keyboard + logger.info('rpc.telegram using custom keyboard from ' + f'config.json: {self._keyboard}') + def _init(self) -> None: """ Initializes this module with the given config, @@ -862,42 +900,7 @@ class Telegram(RPC): :param parse_mode: telegram parse mode :return: None """ - - # default / fallback shortcut buttons - keyboard: List[List[Union[str, KeyboardButton]]] = [ - ['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help'] - ] - - # do not allow commands with mandatory arguments and critical cmds - # like /forcesell and /forcebuy - # TODO: DRY! - its not good to list all valid cmds here. But this - # needs refacoring of the whole telegram module (same problem - # in _help()). - valid_btns: List[str] = ['/start', '/stop', '/status', '/status table', - '/trades', '/profit', '/performance', '/daily', - '/stats', '/count', '/locks', '/balance', - '/stopbuy', '/reload_config', '/show_config', - '/logs', '/whitelist', '/blacklist', '/edge', - '/help', '/version'] - # custom shortcuts specified in config.json - cust_keyboard = self._config['telegram'].get('keyboard', []) - if cust_keyboard: - # check for valid shortcuts - invalid_keys = [b for b in chain.from_iterable(cust_keyboard) - if b not in valid_btns] - if len(invalid_keys): - logger.warning('rpc.telegram: invalid commands for custom ' - f'keyboard: {invalid_keys}') - logger.info('rpc.telegram: using default keyboard.') - else: - keyboard = cust_keyboard - logger.info('rpc.telegram using custom keyboard from ' - f'config.json: {[btn for btn in keyboard]}') - - reply_markup = ReplyKeyboardMarkup(keyboard) - + reply_markup = ReplyKeyboardMarkup(self._keyboard) try: try: self._updater.bot.send_message( diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ce3db7130..148eb6428 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1751,14 +1751,17 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) # no keyboard in config -> default keyboard - telegram._config['telegram']['enabled'] = True + # telegram._config['telegram']['enabled'] = True telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard # invalid keyboard in config -> default keyboard - telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['keyboard'] = invalid_keys_list + freqtradebot.config['telegram']['enabled'] = True + freqtradebot.config['telegram']['keyboard'] = invalid_keys_list + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -1767,6 +1770,11 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: assert log_has('rpc.telegram: using default keyboard.', caplog) # valid keyboard in config -> custom keyboard + freqtradebot.config['telegram']['enabled'] = True + freqtradebot.config['telegram']['keyboard'] = custom_keys_list + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot telegram._config['telegram']['enabled'] = True telegram._config['telegram']['keyboard'] = custom_keys_list telegram._send_msg('test') From 5423c21be0be73139e9ba4488a14ecb5eb53a2ca Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 22:51:40 +0100 Subject: [PATCH 233/281] keyboard type --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1ba45e089..5be880bcc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -7,11 +7,11 @@ import json import logging from datetime import timedelta from itertools import chain -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -85,7 +85,7 @@ class Telegram(RPC): Validates the keyboard configuration from telegram config section. """ - self._keyboard: List[List[str]] = [ + self._keyboard: List[List[Union[str, KeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] From e7e687c8ec84485f45dd8899d587a1560c640220 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:08 +0000 Subject: [PATCH 234/281] Bump requests from 2.25.0 to 2.25.1 Bumps [requests](https://github.com/psf/requests) from 2.25.0 to 2.25.1. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.25.0...v2.25.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3a1a36f..c221338a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ SQLAlchemy==1.3.20 python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.0 -requests==2.25.0 +requests==2.25.1 urllib3==1.26.2 wrapt==1.12.1 jsonschema==3.2.0 From a1755364e1011080f9b443240fb45996a7e51684 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:09 +0000 Subject: [PATCH 235/281] Bump pytest from 6.2.0 to 6.2.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.0 to 6.2.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.0...6.2.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d7570f67..66cb5bd1b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.0 mypy==0.790 -pytest==6.2.0 +pytest==6.2.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From fe27206926b5fcd67c591dff6e0ed2b902454629 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:12 +0000 Subject: [PATCH 236/281] Bump questionary from 1.8.1 to 1.9.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.8.1 to 1.9.0. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.8.1...1.9.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3a1a36f..f27544cff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.8.1 +questionary==1.9.0 prompt-toolkit==3.0.8 From 5716202e45f8651cc700854b6daa4c35c8aa9f4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:24 +0000 Subject: [PATCH 237/281] Bump joblib from 0.17.0 to 1.0.0 Bumps [joblib](https://github.com/joblib/joblib) from 0.17.0 to 1.0.0. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/0.17.0...1.0.0) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 7e480b8c9..c51062bf7 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,5 @@ scipy==1.5.4 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 -joblib==0.17.0 +joblib==1.0.0 progressbar2==3.53.1 From a2873096c8efa911c1a2b5655bac6d4e4f4fc5a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:24 +0000 Subject: [PATCH 238/281] Bump flake8-tidy-imports from 4.2.0 to 4.2.1 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.2.0...4.2.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d7570f67..6e936e91f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.2.0 +flake8-tidy-imports==4.2.1 mypy==0.790 pytest==6.2.0 pytest-asyncio==0.14.0 From 3b67863914fa2b332ec9a80b2176ae4ee20b974f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:27 +0000 Subject: [PATCH 239/281] Bump ccxt from 1.39.33 to 1.39.52 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.33 to 1.39.52. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.33...1.39.52) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3a1a36f..d5dac04fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.5 -ccxt==1.39.33 +ccxt==1.39.52 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From 8eb01302001cad53a6c6d76023893153803e5eff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 08:18:53 +0000 Subject: [PATCH 240/281] Bump pytest-mock from 3.3.1 to 3.4.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.3.1...v3.4.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e2c8b61a1..a2da87430 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.790 pytest==6.2.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.3.1 +pytest-mock==3.4.0 pytest-random-order==1.0.4 isort==5.6.4 From d25fe58574731614c0e9b61afd72271370c5d40b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 08:19:10 +0000 Subject: [PATCH 241/281] Bump sqlalchemy from 1.3.20 to 1.3.22 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.20 to 1.3.22. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 669bc3282..2c565fee5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.1.5 ccxt==1.39.52 aiohttp==3.7.3 -SQLAlchemy==1.3.20 +SQLAlchemy==1.3.22 python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.0 From 277f3ff47b1ce955a44889158374aa5f24283455 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 21 Dec 2020 09:52:10 +0100 Subject: [PATCH 242/281] tests: cleaup --- tests/rpc/test_rpc_telegram.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 148eb6428..26384a507 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1736,9 +1736,6 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: bot = MagicMock() bot.send_message = MagicMock() freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) - telegram._updater = MagicMock() - telegram._updater.bot = bot invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']] default_keys_list = [['/daily', '/profit', '/balance'], @@ -1750,8 +1747,15 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: ['/count', '/start', '/reload_config', '/help']] custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) + def init_telegram(freqtradebot): + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot + return telegram + # no keyboard in config -> default keyboard - # telegram._config['telegram']['enabled'] = True + freqtradebot.config['telegram']['enabled'] = True + telegram = init_telegram(freqtradebot) telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -1759,9 +1763,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # invalid keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = invalid_keys_list - telegram = Telegram(freqtradebot) - telegram._updater = MagicMock() - telegram._updater.bot = bot + telegram = init_telegram(freqtradebot) telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -1772,11 +1774,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # valid keyboard in config -> custom keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = custom_keys_list - telegram = Telegram(freqtradebot) - telegram._updater = MagicMock() - telegram._updater.bot = bot - telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['keyboard'] = custom_keys_list + telegram = init_telegram(freqtradebot) telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard From 2787ba080907092bc517e9cbdc9b7e5f0f1ffc30 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 21 Dec 2020 10:03:27 +0100 Subject: [PATCH 243/281] added /locks to command list --- docs/telegram-usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c940f59ac..965d16d87 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -106,6 +106,7 @@ official commands. You can ask at any moment for help with `/help`. | `/trades [limit]` | List all recently closed trades in a table format. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available +| `/locks` | Show currently locked pairs. | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From 78dff3d5103640aa0efccd610d847168de092d25 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 21 Dec 2020 10:22:24 +0100 Subject: [PATCH 244/281] docs: Note syntax --- docs/telegram-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 950b4df1e..da4a2e8dd 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -111,8 +111,8 @@ You can create your own keyboard in `config.json`: ] }, ``` -!! NOTE: Only a certain list of commands are allowed. Command arguments are not -supported! +!!! Note + Only a certain list of commands are allowed. Command arguments are not supported! ### Supported Commands `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` From 4dadfd199d8485c3920aab5e128d942c191a508f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Dec 2020 07:36:53 +0100 Subject: [PATCH 245/281] Documentation syntax --- docs/telegram-usage.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index da4a2e8dd..c8d95d743 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -88,17 +88,22 @@ Example configuration showing the different settings: ``` ## Create a custom keyboard (command shortcut buttons) + Telegram allows us to create a custom keyboard with buttons for commands. The default custom keyboard looks like this. + ```python [ - ['/daily', '/profit', '/balance'], # row 1, 3 commands - ['/status', '/status table', '/performance'], # row 2, 3 commands - ['/count', '/start', '/stop', '/help'] # row 3, 4 commands + ["/daily", "/profit", "/balance"], # row 1, 3 commands + ["/status", "/status table", "/performance"], # row 2, 3 commands + ["/count", "/start", "/stop", "/help"] # row 3, 4 commands ] -``` +``` + ### Usage + You can create your own keyboard in `config.json`: + ``` json "telegram": { "enabled": true, @@ -107,14 +112,15 @@ You can create your own keyboard in `config.json`: "keyboard": [ ["/daily", "/stats", "/balance", "/profit"], ["/status table", "/performance"], - ["/reload_config", "/count", "/logs"] + ["/reload_config", "/count", "/logs"] ] }, ``` -!!! Note - Only a certain list of commands are allowed. Command arguments are not supported! -### Supported Commands - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + +!!! Note "Supported Commands" + Only the following commands are allowed. Command arguments are not supported! + + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` ## Telegram commands From be28b42bfa232274c7e3f974c3eee2f8878af14c Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 22 Dec 2020 12:34:21 +0100 Subject: [PATCH 246/281] Exception for invalid keyboard config --- freqtrade/rpc/telegram.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5be880bcc..63a98e2b1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -17,6 +17,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ +from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -75,12 +76,12 @@ class Telegram(RPC): self._updater: Updater self._config = freqtrade.config - self._validate_keyboard() + self._init_keyboard() self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() - def _validate_keyboard(self) -> None: + def _init_keyboard(self) -> None: """ Validates the keyboard configuration from telegram config section. @@ -102,19 +103,19 @@ class Telegram(RPC): '/logs', '/whitelist', '/blacklist', '/edge', '/help', '/version'] - # custom shortcuts specified in config.json + # custom keyboard specified in config.json cust_keyboard = self._config['telegram'].get('keyboard', []) if cust_keyboard: # check for valid shortcuts invalid_keys = [b for b in chain.from_iterable(cust_keyboard) if b not in valid_keys] if len(invalid_keys): - logger.warning('rpc.telegram: invalid commands for custom ' - f'keyboard: {invalid_keys}') - logger.info('rpc.telegram: using default keyboard.') + err_msg = ('invalid commands for custom keyboard: ' + f'{invalid_keys}') + raise OperationalException(err_msg) else: self._keyboard = cust_keyboard - logger.info('rpc.telegram using custom keyboard from ' + logger.info('using custom keyboard from ' f'config.json: {self._keyboard}') def _init(self) -> None: From cd1a8e2c42a0bccf6c98381f0835289416311502 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 22 Dec 2020 12:39:27 +0100 Subject: [PATCH 247/281] better error msg --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 63a98e2b1..b520756a9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -110,8 +110,8 @@ class Telegram(RPC): invalid_keys = [b for b in chain.from_iterable(cust_keyboard) if b not in valid_keys] if len(invalid_keys): - err_msg = ('invalid commands for custom keyboard: ' - f'{invalid_keys}') + err_msg = ('config.telegram.keyboard: invalid commands for ' + f'custom keyboard: {invalid_keys}') raise OperationalException(err_msg) else: self._keyboard = cust_keyboard From b1fe5940fa7207e2f0c356f362e199b9c7c9fb91 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 22 Dec 2020 13:01:01 +0100 Subject: [PATCH 248/281] check for Exception and log msgs --- tests/rpc/test_rpc_telegram.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 26384a507..df8983324 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -16,6 +16,7 @@ from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo +from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade @@ -1763,13 +1764,10 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # invalid keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = invalid_keys_list - telegram = init_telegram(freqtradebot) - telegram._send_msg('test') - used_keyboard = bot.send_message.call_args[1]['reply_markup'] - assert used_keyboard == default_keyboard - assert log_has("rpc.telegram: invalid commands for custom keyboard: " - "['/not_valid', '/alsoinvalid']", caplog) - assert log_has('rpc.telegram: using default keyboard.', caplog) + err_msg = re.escape("config.telegram.keyboard: invalid commands for " + "custom keyboard: ['/not_valid', '/alsoinvalid']") + with pytest.raises(OperationalException, match=err_msg): + telegram = init_telegram(freqtradebot) # valid keyboard in config -> custom keyboard freqtradebot.config['telegram']['enabled'] = True @@ -1778,6 +1776,6 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard - assert log_has("rpc.telegram using custom keyboard from config.json: " + assert log_has("using custom keyboard from config.json: " "[['/daily', '/stats', '/balance', '/profit'], ['/count', " "'/start', '/reload_config', '/help']]", caplog) From 36d60fa8a8b7ee6fc2118808aa14d7ef19079111 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 19:54:27 +0200 Subject: [PATCH 249/281] First small compat test --- tests/exchange/test_ccxt_compat.py | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/exchange/test_ccxt_compat.py diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py new file mode 100644 index 000000000..e6deebf62 --- /dev/null +++ b/tests/exchange/test_ccxt_compat.py @@ -0,0 +1,43 @@ +""" +Tests in this file do NOT mock network calls, so they are expected to be fluky at times. + +However, these tests should give a good idea to determine if a new exchange is +suitable to run with freqtrade. + +""" + +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +import pytest + +# Exchanges that should be tested +EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] + + +@pytest.fixture +def exchange_conf(default_conf): + default_conf['exchange']['pair_whitelist'] = [] + return default_conf + + +@pytest.mark.parametrize('exchange', EXCHANGES) +def test_ccxt_fetch_l2_orderbook(exchange_conf, exchange): + + exchange_conf['exchange']['name'] = exchange + exchange_conf['exchange']['name'] = exchange + + exchange = ExchangeResolver.load_exchange(exchange, exchange_conf) + l2 = exchange.fetch_l2_order_book('BTC/USDT') + assert 'asks' in l2 + assert 'bids' in l2 + + for val in [1, 2, 5, 25, 100]: + l2 = exchange.fetch_l2_order_book('BTC/USDT', val) + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert len(l2['asks']) == val + assert len(l2['bids']) == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert len(l2['asks']) == next_limit + assert len(l2['asks']) == next_limit + + From 38af1b2a5dd51ce659b1e7ee13f30237ff332afa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 20:46:01 +0200 Subject: [PATCH 250/281] Improve compat tests --- tests/conftest.py | 6 +- tests/exchange/test_ccxt_compat.py | 109 ++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 965980f7a..e7e9a3096 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -223,7 +223,11 @@ def init_persistence(default_conf): @pytest.fixture(scope="function") -def default_conf(testdatadir): +def default_conf(): + return get_default_conf() + + +def get_default_conf(testdatadir): """ Returns validated configuration suitable for most tests """ configuration = { "max_open_trades": 1, diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index e6deebf62..6b6fbcfc9 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -6,38 +6,95 @@ suitable to run with freqtrade. """ -from freqtrade.resolvers.exchange_resolver import ExchangeResolver import pytest +from pathlib import Path +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_default_conf + # Exchanges that should be tested -EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] +EXCHANGES = { + 'bittrex': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': False + }, + 'binance': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True + }, + 'kraken': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True + }, + 'ftx': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True + } +} -@pytest.fixture -def exchange_conf(default_conf): - default_conf['exchange']['pair_whitelist'] = [] - return default_conf +@pytest.fixture(scope="class") +def exchange_conf(): + config = get_default_conf((Path(__file__).parent / "testdata").resolve()) + config['exchange']['pair_whitelist'] = [] + return config -@pytest.mark.parametrize('exchange', EXCHANGES) -def test_ccxt_fetch_l2_orderbook(exchange_conf, exchange): - - exchange_conf['exchange']['name'] = exchange - exchange_conf['exchange']['name'] = exchange - - exchange = ExchangeResolver.load_exchange(exchange, exchange_conf) - l2 = exchange.fetch_l2_order_book('BTC/USDT') - assert 'asks' in l2 - assert 'bids' in l2 - - for val in [1, 2, 5, 25, 100]: - l2 = exchange.fetch_l2_order_book('BTC/USDT', val) - if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: - assert len(l2['asks']) == val - assert len(l2['bids']) == val - else: - next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) - assert len(l2['asks']) == next_limit - assert len(l2['asks']) == next_limit +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange(request, exchange_conf): + exchange_conf['exchange']['name'] = request.param + exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=False) + yield exchange, request.param +class TestCCXTExchange(): + + def test_load_markets(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + markets = exchange.markets + assert pair in markets + assert isinstance(markets[pair], dict) + + def test_ccxt_fetch_tickers(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + + tickers = exchange.get_tickers() + assert pair in tickers + assert 'ask' in tickers[pair] + assert tickers[pair]['ask'] is not None + assert 'bid' in tickers[pair] + assert tickers[pair]['bid'] is not None + assert 'quoteVolume' in tickers[pair] + if EXCHANGES[exchangename].get('hasQuoteVolume'): + assert tickers[pair]['quoteVolume'] is not None + + def test_ccxt_fetch_ticker(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + + ticker = exchange.fetch_ticker(pair) + assert 'ask' in ticker + assert ticker['ask'] is not None + assert 'bid' in ticker + assert ticker['bid'] is not None + assert 'quoteVolume' in ticker + if EXCHANGES[exchangename].get('hasQuoteVolume'): + assert ticker['quoteVolume'] is not None + + def test_ccxt_fetch_l2_orderbook(self, exchange): + exchange, exchangename = exchange + l2 = exchange.fetch_l2_order_book('BTC/USDT') + assert 'asks' in l2 + assert 'bids' in l2 + + for val in [1, 2, 5, 25, 100]: + l2 = exchange.fetch_l2_order_book('BTC/USDT', val) + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert len(l2['asks']) == val + assert len(l2['bids']) == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert len(l2['asks']) == next_limit + assert len(l2['asks']) == next_limit From 79ed89e4872705f40780266dc08811b1a019e6cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 20:49:46 +0200 Subject: [PATCH 251/281] Add test for fee calculation --- tests/exchange/test_ccxt_compat.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 6b6fbcfc9..082d1dcf4 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -98,3 +98,12 @@ class TestCCXTExchange(): next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit + + def test_ccxt_get_fee(self, exchange): + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + + assert exchange.get_fee(pair, 'limit', 'buy') > 0 < 1 + assert exchange.get_fee(pair, 'limit', 'sell') > 0 < 1 + assert exchange.get_fee(pair, 'market', 'buy') > 0 < 1 + assert exchange.get_fee(pair, 'market', 'sell') > 0 < 1 From 7833d9935cd73cf1e78887308dff64e73fe51605 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 20:50:31 +0200 Subject: [PATCH 252/281] Add dummy test for fetch_ohlcv --- tests/exchange/test_ccxt_compat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 082d1dcf4..c1b8ab6f3 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -99,6 +99,10 @@ class TestCCXTExchange(): assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit + def test_fetch_ohlcv(self, exchange): + # TODO: Implement me + pass + def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] From b7d4ff9c216c7e37d906e5dc06e20daf3c463b35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 13:14:45 +0200 Subject: [PATCH 253/281] Add test for fetch_ohlcv (against exchange) --- tests/exchange/test_ccxt_compat.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index c1b8ab6f3..fa3bd45c8 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -3,7 +3,6 @@ Tests in this file do NOT mock network calls, so they are expected to be fluky a However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. - """ import pytest @@ -16,19 +15,23 @@ from tests.conftest import get_default_conf EXCHANGES = { 'bittrex': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': False + 'hasQuoteVolume': False, + 'timeframe': '5m', }, 'binance': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': True + 'hasQuoteVolume': True, + 'timeframe': '5m', }, 'kraken': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': True + 'hasQuoteVolume': True, + 'timeframe': '5m', }, 'ftx': { 'pair': 'BTC/USDT', - 'hasQuoteVolume': True + 'hasQuoteVolume': True, + 'timeframe': '5m', } } @@ -100,8 +103,13 @@ class TestCCXTExchange(): assert len(l2['asks']) == next_limit def test_fetch_ohlcv(self, exchange): - # TODO: Implement me - pass + exchange, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + timeframe = EXCHANGES[exchangename]['timeframe'] + pair_tf = (pair, timeframe) + ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) + assert isinstance(ohlcv, list) + assert len(exchange.klines(pair_tf)) > 200 def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange From a6e6ce16b116cd299090f133ce5e50c6be06b713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:31:24 +0100 Subject: [PATCH 254/281] Fix test failures --- tests/conftest.py | 4 ++-- tests/exchange/test_ccxt_compat.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e7e9a3096..a57b8c505 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -223,8 +223,8 @@ def init_persistence(default_conf): @pytest.fixture(scope="function") -def default_conf(): - return get_default_conf() +def default_conf(testdatadir): + return get_default_conf(testdatadir) def get_default_conf(testdatadir): diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index fa3bd45c8..1e8ddf319 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -88,12 +88,13 @@ class TestCCXTExchange(): def test_ccxt_fetch_l2_orderbook(self, exchange): exchange, exchangename = exchange - l2 = exchange.fetch_l2_order_book('BTC/USDT') + pair = EXCHANGES[exchangename]['pair'] + l2 = exchange.fetch_l2_order_book(pair) assert 'asks' in l2 assert 'bids' in l2 for val in [1, 2, 5, 25, 100]: - l2 = exchange.fetch_l2_order_book('BTC/USDT', val) + l2 = exchange.fetch_l2_order_book(pair, val) if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: assert len(l2['asks']) == val assert len(l2['bids']) == val From 2016eea2120dab76968f49237e967689a515e3c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:29:39 +0100 Subject: [PATCH 255/281] Fix some test-errors in ccxt_compat --- tests/exchange/test_ccxt_compat.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 1e8ddf319..25b26d489 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -100,8 +100,12 @@ class TestCCXTExchange(): assert len(l2['bids']) == val else: next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) - assert len(l2['asks']) == next_limit - assert len(l2['asks']) == next_limit + if next_limit > 200: + assert len(l2['asks']) > 200 + assert len(l2['asks']) > 200 + else: + assert len(l2['asks']) == next_limit + assert len(l2['asks']) == next_limit def test_fetch_ohlcv(self, exchange): exchange, exchangename = exchange @@ -116,7 +120,7 @@ class TestCCXTExchange(): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] - assert exchange.get_fee(pair, 'limit', 'buy') > 0 < 1 - assert exchange.get_fee(pair, 'limit', 'sell') > 0 < 1 - assert exchange.get_fee(pair, 'market', 'buy') > 0 < 1 - assert exchange.get_fee(pair, 'market', 'sell') > 0 < 1 + assert 0 < exchange.get_fee(pair, 'limit', 'buy') < 1 + assert 0 < exchange.get_fee(pair, 'limit', 'sell') < 1 + assert 0 < exchange.get_fee(pair, 'market', 'buy') < 1 + assert 0 < exchange.get_fee(pair, 'market', 'sell') < 1 From 0981287c62398724ffc16ee80fc7165b3c37c8bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:41:23 +0100 Subject: [PATCH 256/281] Improve test syntax for ccxt_compat tests --- tests/exchange/test_ccxt_compat.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 25b26d489..09cc50f14 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -92,15 +92,16 @@ class TestCCXTExchange(): l2 = exchange.fetch_l2_order_book(pair) assert 'asks' in l2 assert 'bids' in l2 - + l2_limit_range = exchange._ft_has['l2_limit_range'] for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) - if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + if not l2_limit_range or val in l2_limit_range: assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + next_limit = exchange.get_next_limit_in_list(val, l2_limit_range) if next_limit > 200: + # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 else: @@ -116,6 +117,8 @@ class TestCCXTExchange(): assert isinstance(ohlcv, list) assert len(exchange.klines(pair_tf)) > 200 + # TODO: tests fetch_trades (?) + def test_ccxt_get_fee(self, exchange): exchange, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] From b39de171c88af56edc0b9a773a1d507583264c7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:46:08 +0100 Subject: [PATCH 257/281] Don't run longrun regularily --- .github/workflows/ci.yml | 6 ++++++ tests/conftest.py | 13 +++++++++++++ tests/exchange/test_ccxt_compat.py | 1 + 3 files changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36a9fc374..daa10fea7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,12 @@ jobs: - name: Tests run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc + if: matrix.python-version != '3.9' + + - name: Tests incl. ccxt compatibility tests + run: | + pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun + if: matrix.python-version == '3.9' - name: Coveralls if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8') diff --git a/tests/conftest.py b/tests/conftest.py index a57b8c505..9eda0e973 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,19 @@ logging.getLogger('').setLevel(logging.INFO) np.seterr(all='raise') +def pytest_addoption(parser): + parser.addoption('--longrun', action='store_true', dest="longrun", + default=False, help="Enable long-run tests (ccxt compat)") + + +def pytest_configure(config): + config.addinivalue_line( + "markers", "longrun: mark test that is running slowly and should not be run regularily" + ) + if not config.option.longrun: + setattr(config.option, 'markexpr', 'not longrun') + + def log_has(line, logs): # caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar') # and we want to match line against foobar in the tuple diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 09cc50f14..90cdebcdd 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -50,6 +50,7 @@ def exchange(request, exchange_conf): yield exchange, request.param +@pytest.mark.longrun class TestCCXTExchange(): def test_load_markets(self, exchange): From 5599490aa286675d57dfe3860330faf5af23c402 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:50:24 +0100 Subject: [PATCH 258/281] Adjust ohlcv test after rebase --- tests/exchange/test_ccxt_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 90cdebcdd..6d495582b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -115,7 +115,8 @@ class TestCCXTExchange(): timeframe = EXCHANGES[exchangename]['timeframe'] pair_tf = (pair, timeframe) ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) - assert isinstance(ohlcv, list) + assert isinstance(ohlcv, dict) + assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) assert len(exchange.klines(pair_tf)) > 200 # TODO: tests fetch_trades (?) From 65d91b7cbb6ab4a388d66dc2834edc94fecb1cf2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:53:41 +0100 Subject: [PATCH 259/281] Add note on adding new exchange with compat tests --- docs/developer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index dcbaa3ca9..6440dba82 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -242,6 +242,8 @@ The `IProtection` parent class provides a helper method for this in `calculate_l Most exchanges supported by CCXT should work out of the box. +To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun`. + ### Stoploss On Exchange Check if the new exchange supports Stoploss on Exchange orders through their API. From 74bcd82c3d28f7ce6c12118cc63f8f0c689eefeb Mon Sep 17 00:00:00 2001 From: Christof Date: Wed, 23 Dec 2020 16:00:01 +0100 Subject: [PATCH 260/281] Exception msg --- freqtrade/rpc/telegram.py | 5 +++-- tests/rpc/test_rpc_telegram.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b520756a9..e2985fbee 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -110,8 +110,9 @@ class Telegram(RPC): invalid_keys = [b for b in chain.from_iterable(cust_keyboard) if b not in valid_keys] if len(invalid_keys): - err_msg = ('config.telegram.keyboard: invalid commands for ' - f'custom keyboard: {invalid_keys}') + err_msg = ('config.telegram.keyboard: Invalid commands for ' + f'custom Telegram keyboard: {invalid_keys}' + f'\nvalid commands are: {valid_keys}') raise OperationalException(err_msg) else: self._keyboard = cust_keyboard diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index df8983324..b8c5d8858 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1764,8 +1764,9 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # invalid keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = invalid_keys_list - err_msg = re.escape("config.telegram.keyboard: invalid commands for " - "custom keyboard: ['/not_valid', '/alsoinvalid']") + err_msg = re.escape("config.telegram.keyboard: Invalid commands for custom " + "Telegram keyboard: ['/not_valid', '/alsoinvalid']" + "\nvalid commands are: ") + r"*" with pytest.raises(OperationalException, match=err_msg): telegram = init_telegram(freqtradebot) From 721d0fb2a8c4b5c3f582436c05cfa1ff1086475a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 15:55:46 +0100 Subject: [PATCH 261/281] Improve wording of developer docs --- docs/developer.md | 3 ++- tests/exchange/test_ccxt_compat.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 6440dba82..de489a348 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -242,7 +242,8 @@ The `IProtection` parent class provides a helper method for this in `calculate_l Most exchanges supported by CCXT should work out of the box. -To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun`. +To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. +Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). ### Stoploss On Exchange diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 6d495582b..8db56685a 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -5,8 +5,10 @@ However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. """ -import pytest from pathlib import Path + +import pytest + from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_default_conf From 1713841d0b0ed672c9f16229a482497d1b296df3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 16:20:17 +0100 Subject: [PATCH 262/281] Initialize markets at startup for ccxt tests --- tests/exchange/test_ccxt_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 8db56685a..0c8b7bdcf 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -48,7 +48,8 @@ def exchange_conf(): @pytest.fixture(params=EXCHANGES, scope="class") def exchange(request, exchange_conf): exchange_conf['exchange']['name'] = request.param - exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=False) + exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) + yield exchange, request.param From 67193bca3dc6d43bdfdbbbb0a8c461b3569d12ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 16:54:35 +0100 Subject: [PATCH 263/281] Move pairlists to be a plugin submodule --- freqtrade/commands/pairlist_commands.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/{ => plugins}/pairlist/AgeFilter.py | 2 +- freqtrade/{ => plugins}/pairlist/IPairList.py | 0 .../{ => plugins}/pairlist/PerformanceFilter.py | 2 +- .../{ => plugins}/pairlist/PrecisionFilter.py | 2 +- freqtrade/{ => plugins}/pairlist/PriceFilter.py | 2 +- .../{ => plugins}/pairlist/ShuffleFilter.py | 2 +- freqtrade/{ => plugins}/pairlist/SpreadFilter.py | 2 +- .../{ => plugins}/pairlist/StaticPairList.py | 2 +- .../{ => plugins}/pairlist/VolumePairList.py | 2 +- freqtrade/{ => plugins}/pairlist/__init__.py | 0 .../pairlist/rangestabilityfilter.py | 2 +- .../{pairlist => plugins}/pairlistmanager.py | 2 +- freqtrade/resolvers/pairlist_resolver.py | 4 ++-- tests/data/test_dataprovider.py | 2 +- tests/optimize/test_backtesting.py | 16 ++++++++-------- tests/plugins/test_pairlist.py | 4 ++-- 19 files changed, 26 insertions(+), 26 deletions(-) rename freqtrade/{ => plugins}/pairlist/AgeFilter.py (98%) rename freqtrade/{ => plugins}/pairlist/IPairList.py (100%) rename freqtrade/{ => plugins}/pairlist/PerformanceFilter.py (97%) rename freqtrade/{ => plugins}/pairlist/PrecisionFilter.py (97%) rename freqtrade/{ => plugins}/pairlist/PriceFilter.py (98%) rename freqtrade/{ => plugins}/pairlist/ShuffleFilter.py (96%) rename freqtrade/{ => plugins}/pairlist/SpreadFilter.py (96%) rename freqtrade/{ => plugins}/pairlist/StaticPairList.py (97%) rename freqtrade/{ => plugins}/pairlist/VolumePairList.py (98%) rename freqtrade/{ => plugins}/pairlist/__init__.py (100%) rename freqtrade/{ => plugins}/pairlist/rangestabilityfilter.py (98%) rename freqtrade/{pairlist => plugins}/pairlistmanager.py (98%) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index e4ee80ca5..0661cd03c 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -15,7 +15,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: """ Test Pairlist configuration """ - from freqtrade.pairlist.pairlistmanager import PairListManager + from freqtrade.plugins.pairlistmanager import PairListManager config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 08b806076..dc8994d74 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 639904975..49274f75e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,7 +21,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py similarity index 98% rename from freqtrade/pairlist/AgeFilter.py rename to freqtrade/plugins/pairlist/AgeFilter.py index e3465bd82..8c3a5d22f 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -10,7 +10,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py similarity index 100% rename from freqtrade/pairlist/IPairList.py rename to freqtrade/plugins/pairlist/IPairList.py diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py similarity index 97% rename from freqtrade/pairlist/PerformanceFilter.py rename to freqtrade/plugins/pairlist/PerformanceFilter.py index 92a97099e..c99905af5 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.persistence import Trade diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py similarity index 97% rename from freqtrade/pairlist/PrecisionFilter.py rename to freqtrade/plugins/pairlist/PrecisionFilter.py index c0d2893a1..519337f29 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py similarity index 98% rename from freqtrade/pairlist/PriceFilter.py rename to freqtrade/plugins/pairlist/PriceFilter.py index 20a260b46..6558f196f 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py similarity index 96% rename from freqtrade/pairlist/ShuffleFilter.py rename to freqtrade/plugins/pairlist/ShuffleFilter.py index 28778db7b..4d3dd29e3 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -5,7 +5,7 @@ import logging import random from typing import Any, Dict, List -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py similarity index 96% rename from freqtrade/pairlist/SpreadFilter.py rename to freqtrade/plugins/pairlist/SpreadFilter.py index cbbfb9626..2f3fe47e3 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -4,7 +4,7 @@ Spread pair list filter import logging from typing import Any, Dict -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py similarity index 97% rename from freqtrade/pairlist/StaticPairList.py rename to freqtrade/plugins/pairlist/StaticPairList.py index 2879cb364..dd592e0ca 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -7,7 +7,7 @@ import logging from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py similarity index 98% rename from freqtrade/pairlist/VolumePairList.py rename to freqtrade/plugins/pairlist/VolumePairList.py index 7056bc59d..dd8fc64fd 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/__init__.py b/freqtrade/plugins/pairlist/__init__.py similarity index 100% rename from freqtrade/pairlist/__init__.py rename to freqtrade/plugins/pairlist/__init__.py diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py similarity index 98% rename from freqtrade/pairlist/rangestabilityfilter.py rename to freqtrade/plugins/pairlist/rangestabilityfilter.py index 6efe1e2ae..f2e84930b 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py similarity index 98% rename from freqtrade/pairlist/pairlistmanager.py rename to freqtrade/plugins/pairlistmanager.py index 418cc9e92..b71f02898 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -9,7 +9,7 @@ from cachetools import TTLCache, cached from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 4df5da37c..72a3cc1dd 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -6,7 +6,7 @@ This module load custom pairlists import logging from pathlib import Path -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver @@ -20,7 +20,7 @@ class PairListResolver(IResolver): object_type = IPairList object_type_str = "Pairlist" user_subdir = None - initial_search_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() + initial_search_path = Path(__file__).parent.parent.joinpath('plugins/pairlist').resolve() @staticmethod def load_pairlist(pairlist_name: str, exchange, pairlistmanager, diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index a3c57a77b..ee2e551b6 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -6,7 +6,7 @@ from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import ExchangeError, OperationalException -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.state import RunMode from tests.conftest import get_patched_exchange diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 971f8d048..376390664 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -341,7 +341,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' @@ -372,7 +372,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = "1m" @@ -392,7 +392,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=[])) default_conf['timeframe'] = "1m" @@ -415,9 +415,9 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['XRP/BTC'])) - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.refresh_pairlist') + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist') default_conf['ticker_interval'] = "1m" default_conf['datadir'] = testdatadir @@ -700,7 +700,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) patched_configuration_load_config_file(mocker, default_conf) @@ -740,7 +740,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS + ['profit_abs'])) - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) text_table_mock = MagicMock() @@ -837,7 +837,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] }), ]) - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index c4b370e15..1795fc27f 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -6,7 +6,7 @@ import pytest from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver from tests.conftest import get_patched_freqtradebot, log_has, log_has_re @@ -190,7 +190,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ ) # Remove caching of ticker data to emulate changing volume by the time of second call mocker.patch.multiple( - 'freqtrade.pairlist.pairlistmanager.PairListManager', + 'freqtrade.plugins.pairlistmanager.PairListManager', _get_cached_tickers=MagicMock(return_value=tickers_dict), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2) From f11fd2fee10231ecd1db5f286578f4e71702f44a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 17:00:02 +0100 Subject: [PATCH 264/281] Sort imports --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dc8994d74..d60b111f2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,8 +22,8 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin -from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 49274f75e..a689786ec 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,8 +21,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index c99905af5..7d91bb77c 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -6,8 +6,8 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.persistence import Trade +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) From 516e56bfaa638dbea867f7ed824255bfc2647f90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 20:50:32 +0100 Subject: [PATCH 265/281] Move init of _config to apiserver parent --- freqtrade/rpc/api_server.py | 1 - freqtrade/rpc/rpc.py | 1 + freqtrade/rpc/telegram.py | 1 - freqtrade/rpc/webhook.py | 1 - 4 files changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d9bf4d14a..31e7f3ff2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -98,7 +98,6 @@ class ApiServer(RPC): """ super().__init__(freqtrade) - self._config = freqtrade.config self.app = Flask(__name__) self._cors = CORS(self.app, resources={r"/api/*": { diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2bc989e80..9b7d62b54 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -79,6 +79,7 @@ class RPC: :return: None """ self._freqtrade = freqtrade + self._config: Dict[str, Any] = freqtrade.config @property def name(self) -> str: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e2985fbee..dddba7457 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -75,7 +75,6 @@ class Telegram(RPC): super().__init__(freqtrade) self._updater: Updater - self._config = freqtrade.config self._init_keyboard() self._init() if self._config.get('fiat_display_currency', None): diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 21413f165..f4008a70f 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -25,7 +25,6 @@ class Webhook(RPC): """ super().__init__(freqtrade) - self._config = freqtrade.config self._url = self._config['webhook']['url'] def cleanup(self) -> None: From 4cbbb80bc3a1717642a73467d9351d8b7bf2dee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 07:10:01 +0100 Subject: [PATCH 266/281] Refactor test_telegram to simplify tests --- tests/rpc/test_rpc_telegram.py | 169 +++++++++++++-------------------- 1 file changed, 65 insertions(+), 104 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b8c5d8858..71782411b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -54,11 +54,18 @@ class DummyCls(Telegram): raise Exception('test') -def test__init__(default_conf, mocker) -> None: +def get_telegram_testobject(mocker, default_conf): + ftbot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(ftbot) + + return telegram, ftbot + + +def test_telegram__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) + telegram, _ = get_telegram_testobject(mocker, default_conf) assert telegram._config == default_conf @@ -66,7 +73,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) - Telegram(get_patched_freqtradebot(mocker, default_conf)) + get_telegram_testobject(mocker, default_conf) assert start_polling.call_count == 0 # number of handles registered @@ -88,7 +95,7 @@ def test_cleanup(default_conf, mocker, ) -> None: updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) - telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.cleanup() assert telegram._updater.stop.call_count == 1 @@ -180,8 +187,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -208,12 +214,10 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED # Status is also enabled when stopped telegram._status(update=update, context=MagicMock()) @@ -257,10 +261,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: ) default_conf['stake_amount'] = 15.0 - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot, (True, False)) freqtradebot.state = State.STOPPED # Status table is also enabled when stopped @@ -308,9 +312,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -386,9 +390,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Try invalid data msg_mock.reset_mock() @@ -425,9 +428,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -519,11 +521,9 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 @@ -548,11 +548,9 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - freqtradebot.config['dry_run'] = False telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] @@ -570,11 +568,9 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 @@ -608,11 +604,9 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 # Test if wrap happens around 4000 - @@ -630,8 +624,7 @@ def test_start_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -648,8 +641,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -667,8 +659,7 @@ def test_stop_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -686,8 +677,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -705,8 +695,7 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 telegram._stopbuy(update=update, context=MagicMock()) @@ -724,8 +713,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -909,9 +897,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Trader is not running freqtradebot.state = State.STOPPED @@ -1014,9 +1001,8 @@ def test_performance_handle(default_conf, update, ticker, fee, fetch_ticker=ticker, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -1049,9 +1035,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED telegram._count(update=update, context=MagicMock()) @@ -1085,9 +1070,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None fetch_ticker=ticker, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') @@ -1110,9 +1094,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1131,9 +1113,7 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1148,9 +1128,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1190,9 +1168,8 @@ def test_telegram_logs(default_conf, update, mocker) -> None: ) setup_logging(default_conf) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, _ = get_telegram_testobject(mocker, default_conf) - telegram = Telegram(freqtradebot) context = MagicMock() context.args = [] telegram._logs(update=update, context=context) @@ -1223,9 +1200,7 @@ def test_edge_disabled(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1245,9 +1220,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, edge_conf) - - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, edge_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1263,8 +1236,8 @@ def test_telegram_trades(mocker, update, default_conf, fee): _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + context = MagicMock() context.args = [] @@ -1299,8 +1272,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1325,9 +1297,7 @@ def test_help_handle(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._help(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1341,8 +1311,7 @@ def test_version_handle(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._version(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1357,8 +1326,8 @@ def test_show_config_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) default_conf['runmode'] = RunMode.DRY_RUN - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1398,8 +1367,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) } - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ @@ -1431,8 +1400,8 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Bittrex', @@ -1450,8 +1419,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ @@ -1520,8 +1489,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ @@ -1554,8 +1523,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, 'status': 'running' @@ -1570,8 +1538,7 @@ def test_warning_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' @@ -1586,8 +1553,7 @@ def test_startup_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': '*Custom:* `Hello World`' @@ -1602,8 +1568,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): telegram.send_msg({ 'type': None, @@ -1618,8 +1583,8 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', @@ -1649,8 +1614,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -1696,8 +1660,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) assert telegram._get_sell_emoji(msg) == expected @@ -1705,8 +1668,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): def test__send_msg(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._updater = MagicMock() telegram._updater.bot = bot @@ -1719,8 +1681,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._updater = MagicMock() telegram._updater.bot = bot From be4a4be7a3a34d19f05d2f3932325afd8692ce82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 07:29:26 +0100 Subject: [PATCH 267/281] Further simplify test_telegram --- tests/rpc/test_rpc_telegram.py | 373 ++++++--------------------------- 1 file changed, 69 insertions(+), 304 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 71782411b..dc8ff46c1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -54,18 +54,25 @@ class DummyCls(Telegram): raise Exception('test') -def get_telegram_testobject(mocker, default_conf): +def get_telegram_testobject(mocker, default_conf, mock=True): + msg_mock = MagicMock() + if mock: + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) ftbot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(ftbot) - return telegram, ftbot + return telegram, ftbot, msg_mock def test_telegram__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf) assert telegram._config == default_conf @@ -73,7 +80,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) - get_telegram_testobject(mocker, default_conf) + get_telegram_testobject(mocker, default_conf, mock=False) assert start_polling.call_count == 0 # number of handles registered @@ -95,7 +102,7 @@ def test_cleanup(default_conf, mocker, ) -> None: updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) telegram.cleanup() assert telegram._updater.stop.call_count == 1 @@ -152,11 +159,9 @@ def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" - msg_mock = MagicMock() status_table = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', @@ -184,10 +189,9 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'is_open': True }]), _status_table=status_table, - _send_msg=msg_mock ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -205,16 +209,13 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() status_table = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), _status_table=status_table, - _send_msg=msg_mock ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) @@ -253,16 +254,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) default_conf['stake_amount'] = 15.0 - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) @@ -305,14 +300,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) @@ -383,14 +372,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) # Try invalid data @@ -421,14 +404,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._profit(update=update, context=MagicMock()) @@ -480,16 +457,8 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -514,14 +483,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._balance(update=update, context=MagicMock()) @@ -541,14 +503,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) freqtradebot.config['dry_run'] = False @@ -561,14 +516,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._balance(update=update, context=MagicMock()) @@ -597,14 +545,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'value': 1000.0, }) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._balance(update=update, context=MagicMock()) @@ -617,14 +558,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None def test_start_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -634,14 +569,8 @@ def test_start_handle(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -652,14 +581,8 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_stop_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -670,14 +593,8 @@ def test_stop_handle(default_conf, update, mocker) -> None: def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -688,14 +605,8 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_stopbuy_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 telegram._stopbuy(update=update, context=MagicMock()) @@ -706,14 +617,8 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: def test_reload_config_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -890,14 +795,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) # Trader is not running @@ -990,18 +889,13 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) + mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) # Create some test data @@ -1024,18 +918,12 @@ def test_performance_handle(default_conf, update, ticker, fee, def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) freqtradebot.state = State.STOPPED @@ -1059,18 +947,12 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') @@ -1088,13 +970,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None def test_whitelist_static(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1103,17 +980,11 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1122,13 +993,8 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: def test_blacklist_static(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1160,15 +1026,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None: def test_telegram_logs(default_conf, update, mocker) -> None: - msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _send_msg=msg_mock ) setup_logging(default_conf) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1193,14 +1057,8 @@ def test_telegram_logs(default_conf, update, mocker) -> None: def test_edge_disabled(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1208,19 +1066,13 @@ def test_edge_disabled(default_conf, update, mocker) -> None: def test_edge_enabled(edge_conf, update, mocker) -> None: - msg_mock = MagicMock() mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, edge_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1229,14 +1081,8 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: def test_telegram_trades(mocker, update, default_conf, fee): - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1265,14 +1111,8 @@ def test_telegram_trades(mocker, update, default_conf, fee): def test_telegram_delete_trade(mocker, update, default_conf, fee): - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1291,13 +1131,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): def test_help_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._help(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1305,13 +1139,8 @@ def test_help_handle(default_conf, update, mocker) -> None: def test_version_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._version(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1319,15 +1148,10 @@ def test_version_handle(default_conf, update, mocker) -> None: def test_show_config_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) + default_conf['runmode'] = RunMode.DRY_RUN - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1347,12 +1171,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) + msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', @@ -1367,7 +1186,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) } - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ @@ -1394,13 +1213,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, @@ -1413,13 +1227,8 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 @@ -1483,13 +1292,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 @@ -1517,13 +1321,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: def test_send_msg_status_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, 'status': 'running' @@ -1532,13 +1331,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: def test_warning_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' @@ -1547,13 +1340,7 @@ def test_warning_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': '*Custom:* `Hello World`' @@ -1562,13 +1349,7 @@ def test_startup_notification(default_conf, mocker) -> None: def test_send_msg_unknown_type(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): telegram.send_msg({ 'type': None, @@ -1577,13 +1358,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None: def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: del default_conf['fiat_display_currency'] - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, @@ -1608,13 +1383,8 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: del default_conf['fiat_display_currency'] - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -1654,13 +1424,8 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: ]) def test__sell_emoji(default_conf, mocker, msg, expected): del default_conf['fiat_display_currency'] - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, _ = get_telegram_testobject(mocker, default_conf) assert telegram._get_sell_emoji(msg) == expected @@ -1668,7 +1433,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): def test__send_msg(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) telegram._updater = MagicMock() telegram._updater.bot = bot @@ -1681,7 +1446,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) telegram._updater = MagicMock() telegram._updater.bot = bot From 5bf739b917d8a32872c8f1e5e32722f1c5a2d7ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 07:39:46 +0100 Subject: [PATCH 268/281] Simplify more telegram tests --- tests/rpc/test_rpc_telegram.py | 52 +++++++++++++--------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index dc8ff46c1..5040f35cf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -6,7 +6,7 @@ import re from datetime import datetime from random import choice, randint from string import ascii_uppercase -from unittest.mock import ANY, MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock import arrow import pytest @@ -631,7 +631,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: def test_telegram_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) patch_whitelist(mocker, default_conf) @@ -659,8 +659,8 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 3 - last_msg = rpc_mock.call_args_list[-1][0][0] + assert msg_mock.call_count == 3 + last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -686,7 +686,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) patch_whitelist(mocker, default_conf) @@ -718,9 +718,9 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 3 + assert msg_mock.call_count == 3 - last_msg = rpc_mock.call_args_list[-1][0][0] + last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -746,7 +746,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_whitelist(mocker, default_conf) mocker.patch.multiple( @@ -761,7 +761,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None # Create some test data freqtradebot.enter_positions() - rpc_mock.reset_mock() + msg_mock.reset_mock() # /forcesell all context = MagicMock() @@ -769,8 +769,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None telegram._forcesell(update=update, context=context) # Called for each trade 3 times - assert rpc_mock.call_count == 8 - msg = rpc_mock.call_args_list[1][0][0] + assert msg_mock.call_count == 8 + msg = msg_mock.call_args_list[1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -828,21 +828,14 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: +def test_forcebuy_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) - mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - markets=PropertyMock(markets), - ) + fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) - freqtradebot = FreqtradeBot(default_conf) + telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # /forcebuy ETH/BTC context = MagicMock() @@ -867,24 +860,17 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None: +def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - markets=PropertyMock(markets), - ) - freqtradebot = FreqtradeBot(default_conf) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) update.message.text = '/forcebuy ETH/Nonepair' telegram._forcebuy(update=update, context=MagicMock()) - assert rpc_mock.call_count == 1 - assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' + assert msg_mock.call_count == 1 + assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' def test_performance_handle(default_conf, update, ticker, fee, From 1508e08ea5f60b6dbcd5ad4faf085170f09cecd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 08:36:35 +0100 Subject: [PATCH 269/281] Move fiatconvert init to RPC class --- freqtrade/rpc/api_server.py | 4 ---- freqtrade/rpc/rpc.py | 4 +++- freqtrade/rpc/telegram.py | 3 --- tests/rpc/test_rpc.py | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 31e7f3ff2..804c83207 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -20,7 +20,6 @@ from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade -from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPC, RPCException @@ -117,9 +116,6 @@ class ApiServer(RPC): # Register application handling self.register_rest_rpc_urls() - if self._config.get('fiat_display_currency', None): - self._fiat_converter = CryptoToFiatConverter() - thread = threading.Thread(target=self.run, daemon=True) thread.start() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b7d62b54..8c8e42c28 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -69,7 +69,7 @@ class RPC: """ RPC class can be used to have extra feature, like bot data, and access to DB data """ - # Bind _fiat_converter if needed in each RPC handler + # Bind _fiat_converter if needed _fiat_converter: Optional[CryptoToFiatConverter] = None def __init__(self, freqtrade) -> None: @@ -80,6 +80,8 @@ class RPC: """ self._freqtrade = freqtrade self._config: Dict[str, Any] = freqtrade.config + if self._config.get('fiat_display_currency', None): + self._fiat_converter = CryptoToFiatConverter() @property def name(self) -> str: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index dddba7457..e15071845 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -19,7 +19,6 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC, RPCException, RPCMessageType -from freqtrade.rpc.fiat_convert import CryptoToFiatConverter logger = logging.getLogger(__name__) @@ -77,8 +76,6 @@ class Telegram(RPC): self._updater: Updater self._init_keyboard() self._init() - if self._config.get('fiat_display_currency', None): - self._fiat_converter = CryptoToFiatConverter() def _init_keyboard(self) -> None: """ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4b36f4b4e..19788c067 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -185,7 +185,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - + del default_conf['fiat_display_currency'] freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) From a87c273903743d74e9c8ba5de866159669d2dd80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 09:01:53 +0100 Subject: [PATCH 270/281] Refactor RPC modules so handlers don't inherit RPC directly --- freqtrade/rpc/__init__.py | 2 +- freqtrade/rpc/api_server.py | 69 +++++++++++++++++---------------- freqtrade/rpc/rpc.py | 39 ++++++++++++------- freqtrade/rpc/rpc_manager.py | 19 ++++----- freqtrade/rpc/telegram.py | 69 +++++++++++++++++---------------- freqtrade/rpc/webhook.py | 13 ++++--- tests/rpc/test_rpc_apiserver.py | 11 +++--- tests/rpc/test_rpc_telegram.py | 54 ++++++++++++++++---------- tests/rpc/test_rpc_webhook.py | 12 +++--- 9 files changed, 160 insertions(+), 128 deletions(-) diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 88978519b..0a0130ca7 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa: F401 -from .rpc import RPC, RPCException, RPCMessageType +from .rpc import RPC, RPCException, RPCHandler, RPCMessageType from .rpc_manager import RPCManager diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 804c83207..b489586c8 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -20,7 +20,7 @@ from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler logger = logging.getLogger(__name__) @@ -78,7 +78,7 @@ def shutdown_session(exception=None): Trade.session.remove() -class ApiServer(RPC): +class ApiServer(RPCHandler): """ This class runs api server and provides rpc.rpc functionality to it @@ -89,13 +89,14 @@ class ApiServer(RPC): return (safe_str_cmp(username, self._config['api_server'].get('username')) and safe_str_cmp(password, self._config['api_server'].get('password'))) - def __init__(self, freqtrade) -> None: + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: """ - Init the api server, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot + Init the api server, and init the super class RPCHandler + :param rpc: instance of RPC Helper class + :param config: Configuration object :return: None """ - super().__init__(freqtrade) + super().__init__(rpc, config) self.app = Flask(__name__) self._cors = CORS(self.app, @@ -282,7 +283,7 @@ class ApiServer(RPC): Handler for /start. Starts TradeThread in bot if stopped. """ - msg = self._rpc_start() + msg = self._rpc._rpc_start() return jsonify(msg) @require_login @@ -292,7 +293,7 @@ class ApiServer(RPC): Handler for /stop. Stops TradeThread in bot if running """ - msg = self._rpc_stop() + msg = self._rpc._rpc_stop() return jsonify(msg) @require_login @@ -302,7 +303,7 @@ class ApiServer(RPC): Handler for /stopbuy. Sets max_open_trades to 0 and gracefully sells all open trades """ - msg = self._rpc_stopbuy() + msg = self._rpc._rpc_stopbuy() return jsonify(msg) @rpc_catch_errors @@ -326,7 +327,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return jsonify(RPC._rpc_show_config(self._config, self._freqtrade.state)) + return jsonify(RPC._rpc_show_config(self._config, self._rpc._freqtrade.state)) @require_login @rpc_catch_errors @@ -335,7 +336,7 @@ class ApiServer(RPC): Handler for /reload_config. Triggers a config file reload """ - msg = self._rpc_reload_config() + msg = self._rpc._rpc_reload_config() return jsonify(msg) @require_login @@ -345,7 +346,7 @@ class ApiServer(RPC): Handler for /count. Returns the number of trades running """ - msg = self._rpc_count() + msg = self._rpc._rpc_count() return jsonify(msg) @require_login @@ -355,7 +356,7 @@ class ApiServer(RPC): Handler for /locks. Returns the currently active locks. """ - return jsonify(self._rpc_locks()) + return jsonify(self._rpc._rpc_locks()) @require_login @rpc_catch_errors @@ -368,10 +369,10 @@ class ApiServer(RPC): timescale = request.args.get('timescale', 7) timescale = int(timescale) - stats = self._rpc_daily_profit(timescale, - self._config['stake_currency'], - self._config.get('fiat_display_currency', '') - ) + stats = self._rpc._rpc_daily_profit(timescale, + self._config['stake_currency'], + self._config.get('fiat_display_currency', '') + ) return jsonify(stats) @@ -394,7 +395,7 @@ class ApiServer(RPC): Returns information related to Edge. :return: edge stats """ - stats = self._rpc_edge() + stats = self._rpc._rpc_edge() return jsonify(stats) @@ -408,9 +409,9 @@ class ApiServer(RPC): :return: stats """ - stats = self._rpc_trade_statistics(self._config['stake_currency'], - self._config.get('fiat_display_currency') - ) + stats = self._rpc._rpc_trade_statistics(self._config['stake_currency'], + self._config.get('fiat_display_currency') + ) return jsonify(stats) @@ -422,7 +423,7 @@ class ApiServer(RPC): Returns a Object with "durations" and "sell_reasons" as keys. """ - stats = self._rpc_stats() + stats = self._rpc._rpc_stats() return jsonify(stats) @@ -435,7 +436,7 @@ class ApiServer(RPC): Returns a cumulative performance statistics :return: stats """ - stats = self._rpc_performance() + stats = self._rpc._rpc_performance() return jsonify(stats) @@ -448,7 +449,7 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ try: - results = self._rpc_trade_status() + results = self._rpc._rpc_trade_status() return jsonify(results) except RPCException: return jsonify([]) @@ -461,8 +462,8 @@ class ApiServer(RPC): Returns the current status of the trades in json format """ - results = self._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + results = self._rpc._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) return jsonify(results) @require_login @@ -474,7 +475,7 @@ class ApiServer(RPC): Returns the X last trades in json format """ limit = int(request.args.get('limit', 0)) - results = self._rpc_trade_history(limit) + results = self._rpc._rpc_trade_history(limit) return jsonify(results) @require_login @@ -487,7 +488,7 @@ class ApiServer(RPC): param: tradeid: Numeric trade-id assigned to the trade. """ - result = self._rpc_delete(tradeid) + result = self._rpc._rpc_delete(tradeid) return jsonify(result) @require_login @@ -496,7 +497,7 @@ class ApiServer(RPC): """ Handler for /whitelist. """ - results = self._rpc_whitelist() + results = self._rpc._rpc_whitelist() return jsonify(results) @require_login @@ -506,7 +507,7 @@ class ApiServer(RPC): Handler for /blacklist. """ add = request.json.get("blacklist", None) if request.method == 'POST' else None - results = self._rpc_blacklist(add) + results = self._rpc._rpc_blacklist(add) return jsonify(results) @require_login @@ -519,7 +520,7 @@ class ApiServer(RPC): price = request.json.get("price", None) price = float(price) if price is not None else price - trade = self._rpc_forcebuy(asset, price) + trade = self._rpc._rpc_forcebuy(asset, price) if trade: return jsonify(trade.to_json()) else: @@ -532,7 +533,7 @@ class ApiServer(RPC): Handler for /forcesell. """ tradeid = request.json.get("tradeid") - results = self._rpc_forcesell(tradeid) + results = self._rpc._rpc_forcesell(tradeid) return jsonify(results) @require_login @@ -554,7 +555,7 @@ class ApiServer(RPC): if not pair or not timeframe: return self.rest_error("Mandatory parameter missing.", 400) - results = self._rpc_analysed_dataframe(pair, timeframe, limit) + results = self._rpc._rpc_analysed_dataframe(pair, timeframe, limit) return jsonify(results) @require_login @@ -593,7 +594,7 @@ class ApiServer(RPC): """ Handler for /plot_config. """ - return jsonify(self._rpc_plot_config()) + return jsonify(self._rpc._rpc_plot_config()) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8c8e42c28..42ab76622 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -65,6 +65,32 @@ class RPCException(Exception): } +class RPCHandler: + + def __init__(self, rpc: 'RPC', config: Dict[str, Any]) -> None: + """ + Initializes RPCHandlers + :param rpc: instance of RPC Helper class + :param config: Configuration object + :return: None + """ + self._rpc = rpc + self._config: Dict[str, Any] = config + + @property + def name(self) -> str: + """ Returns the lowercase name of the implementation """ + return self.__class__.__name__.lower() + + @abstractmethod + def cleanup(self) -> None: + """ Cleanup pending module resources """ + + @abstractmethod + def send_msg(self, msg: Dict[str, str]) -> None: + """ Sends a message to all registered rpc modules """ + + class RPC: """ RPC class can be used to have extra feature, like bot data, and access to DB data @@ -83,19 +109,6 @@ class RPC: if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() - @property - def name(self) -> str: - """ Returns the lowercase name of the implementation """ - return self.__class__.__name__.lower() - - @abstractmethod - def cleanup(self) -> None: - """ Cleanup pending module resources """ - - @abstractmethod - def send_msg(self, msg: Dict[str, str]) -> None: - """ Sends a message to all registered rpc modules """ - @staticmethod def _rpc_show_config(config, botstate: State) -> Dict[str, Any]: """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index c42878f99..38a4e95fd 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -4,7 +4,7 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) import logging from typing import Any, Dict, List -from freqtrade.rpc import RPC, RPCMessageType +from freqtrade.rpc import RPC, RPCHandler, RPCMessageType logger = logging.getLogger(__name__) @@ -16,25 +16,26 @@ class RPCManager: """ def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules """ - self.registered_modules: List[RPC] = [] - + self.registered_modules: List[RPCHandler] = [] + self._rpc = RPC(freqtrade) + config = freqtrade.config # Enable telegram - if freqtrade.config.get('telegram', {}).get('enabled', False): + if config.get('telegram', {}).get('enabled', False): logger.info('Enabling rpc.telegram ...') from freqtrade.rpc.telegram import Telegram - self.registered_modules.append(Telegram(freqtrade)) + self.registered_modules.append(Telegram(self._rpc, config)) # Enable Webhook - if freqtrade.config.get('webhook', {}).get('enabled', False): + if config.get('webhook', {}).get('enabled', False): logger.info('Enabling rpc.webhook ...') from freqtrade.rpc.webhook import Webhook - self.registered_modules.append(Webhook(freqtrade)) + self.registered_modules.append(Webhook(self._rpc, config)) # Enable local rest api server for cmd line control - if freqtrade.config.get('api_server', {}).get('enabled', False): + if config.get('api_server', {}).get('enabled', False): logger.info('Enabling rpc.api_server') from freqtrade.rpc.api_server import ApiServer - self.registered_modules.append(ApiServer(freqtrade)) + self.registered_modules.append(ApiServer(self._rpc, config)) def cleanup(self) -> None: """ Stops all enabled rpc modules """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e15071845..7ec67e5d0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -18,7 +18,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException -from freqtrade.rpc import RPC, RPCException, RPCMessageType +from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType logger = logging.getLogger(__name__) @@ -62,16 +62,18 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: return wrapper -class Telegram(RPC): +class Telegram(RPCHandler): """ This class handles all telegram communication """ - def __init__(self, freqtrade) -> None: + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: + """ - Init the Telegram call, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot + Init the Telegram call, and init the super class RPCHandler + :param rpc: instance of RPC Helper class + :param config: Configuration object :return: None """ - super().__init__(freqtrade) + super().__init__(rpc, config) self._updater: Updater self._init_keyboard() @@ -181,8 +183,8 @@ class Telegram(RPC): return if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._fiat_converter: - msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) else: msg['stake_amount_fiat'] = 0 @@ -222,8 +224,8 @@ class Telegram(RPC): # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._fiat_converter): - msg['profit_fiat'] = self._fiat_converter.convert_amount( + and self._rpc._fiat_converter): + msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) message += (' `({gain}: {profit_amount:.8f} {stake_currency}' ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) @@ -275,7 +277,7 @@ class Telegram(RPC): return try: - results = self._rpc_trade_status() + results = self._rpc._rpc_trade_status() messages = [] for r in results: @@ -325,8 +327,9 @@ class Telegram(RPC): :return: None """ try: - statlist, head = self._rpc_status_table(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + statlist, head = self._rpc._rpc_status_table( + self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + message = tabulate(statlist, headers=head, tablefmt='simple') self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: @@ -348,7 +351,7 @@ class Telegram(RPC): except (TypeError, ValueError, IndexError): timescale = 7 try: - stats = self._rpc_daily_profit( + stats = self._rpc._rpc_daily_profit( timescale, stake_cur, fiat_disp_cur @@ -382,7 +385,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') - stats = self._rpc_trade_statistics( + stats = self._rpc._rpc_trade_statistics( stake_cur, fiat_disp_cur) profit_closed_coin = stats['profit_closed_coin'] @@ -433,7 +436,7 @@ class Telegram(RPC): Handler for /stats Show stats of recent trades """ - stats = self._rpc_stats() + stats = self._rpc._rpc_stats() reason_map = { 'roi': 'ROI', @@ -473,8 +476,8 @@ class Telegram(RPC): def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ try: - result = self._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + result = self._rpc._rpc_balance(self._config['stake_currency'], + self._config.get('fiat_display_currency', '')) output = '' if self._config['dry_run']: @@ -517,7 +520,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_start() + msg = self._rpc._rpc_start() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -529,7 +532,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_stop() + msg = self._rpc._rpc_stop() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -541,7 +544,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_reload_config() + msg = self._rpc._rpc_reload_config() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -553,7 +556,7 @@ class Telegram(RPC): :param update: message update :return: None """ - msg = self._rpc_stopbuy() + msg = self._rpc._rpc_stopbuy() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -571,7 +574,7 @@ class Telegram(RPC): self._send_msg("You must specify a trade-id or 'all'.") return try: - msg = self._rpc_forcesell(trade_id) + msg = self._rpc._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) except RPCException as e: @@ -590,7 +593,7 @@ class Telegram(RPC): pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None try: - self._rpc_forcebuy(pair, price) + self._rpc._rpc_forcebuy(pair, price) except RPCException as e: self._send_msg(str(e)) @@ -609,7 +612,7 @@ class Telegram(RPC): except (TypeError, ValueError, IndexError): nrecent = 10 try: - trades = self._rpc_trade_history( + trades = self._rpc._rpc_trade_history( nrecent ) trades_tab = tabulate( @@ -642,7 +645,7 @@ class Telegram(RPC): if not context.args or len(context.args) == 0: raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) - msg = self._rpc_delete(trade_id) + msg = self._rpc._rpc_delete(trade_id) self._send_msg(( '`{result_msg}`\n' 'Please make sure to take care of this asset on the exchange manually.' @@ -661,7 +664,7 @@ class Telegram(RPC): :return: None """ try: - trades = self._rpc_performance() + trades = self._rpc._rpc_performance() stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( index=i + 1, pair=trade['pair'], @@ -683,7 +686,7 @@ class Telegram(RPC): :return: None """ try: - counts = self._rpc_count() + counts = self._rpc._rpc_count() message = tabulate({k: [v] for k, v in counts.items()}, headers=['current', 'max', 'total stake'], tablefmt='simple') @@ -700,7 +703,7 @@ class Telegram(RPC): Returns the currently active locks """ try: - locks = self._rpc_locks() + locks = self._rpc._rpc_locks() message = tabulate([[ lock['pair'], lock['lock_end_time'], @@ -720,7 +723,7 @@ class Telegram(RPC): Shows the currently active whitelist """ try: - whitelist = self._rpc_whitelist() + whitelist = self._rpc._rpc_whitelist() message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" @@ -738,7 +741,7 @@ class Telegram(RPC): """ try: - blacklist = self._rpc_blacklist(context.args) + blacklist = self._rpc._rpc_blacklist(context.args) errmsgs = [] for pair, error in blacklist['errors'].items(): errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") @@ -792,7 +795,7 @@ class Telegram(RPC): Shows information related to Edge """ try: - edge_pairs = self._rpc_edge() + 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) @@ -862,7 +865,7 @@ class Telegram(RPC): :param update: message update :return: None """ - val = RPC._rpc_show_config(self._freqtrade.config, self._freqtrade.state) + val = RPC._rpc_show_config(self._config, self._rpc._freqtrade.state) if val['trailing_stop']: sl_info = ( diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index f4008a70f..5796201b5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -6,7 +6,7 @@ from typing import Any, Dict from requests import RequestException, post -from freqtrade.rpc import RPC, RPCMessageType +from freqtrade.rpc import RPC, RPCHandler, RPCMessageType logger = logging.getLogger(__name__) @@ -14,16 +14,17 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.webhook ...') -class Webhook(RPC): +class Webhook(RPCHandler): """ This class handles all webhook communication """ - def __init__(self, freqtrade) -> None: + def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: """ - Init the Webhook class, and init the super class RPC - :param freqtrade: Instance of a freqtrade bot + Init the Webhook class, and init the super class RPCHandler + :param rpc: instance of RPC Helper class + :param config: Configuration object :return: None """ - super().__init__(freqtrade) + super().__init__(rpc, config) self._url = self._config['webhook']['url'] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a1f4f7c9d..e7eee6f05 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -13,6 +13,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade +from freqtrade.rpc import RPC from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -36,8 +37,9 @@ def botclient(default_conf, mocker): }}) ftbot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(ftbot) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) - apiserver = ApiServer(ftbot) + apiserver = ApiServer(rpc, default_conf) yield ftbot, apiserver.app.test_client() # Cleanup ... ? @@ -179,8 +181,7 @@ def test_api__init__(default_conf, mocker): }}) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) - - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert apiserver._config == default_conf @@ -197,7 +198,7 @@ def test_api_run(default_conf, mocker, caplog): server_mock = MagicMock() mocker.patch('freqtrade.rpc.api_server.make_server', server_mock) - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert apiserver._config == default_conf apiserver.run() @@ -251,7 +252,7 @@ def test_api_cleanup(default_conf, mocker, caplog): mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock()) mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock()) - apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf)) + apiserver = ApiServer(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) apiserver.run() stop_mock = MagicMock() stop_mock.shutdown = MagicMock() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5040f35cf..97b9e5e7c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -20,7 +20,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade -from freqtrade.rpc import RPCMessageType +from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellType @@ -32,8 +32,8 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ - def __init__(self, freqtrade) -> None: - super().__init__(freqtrade) + def __init__(self, rpc: RPC, config) -> None: + super().__init__(rpc, config) self.state = {'called': False} def _init(self): @@ -54,7 +54,7 @@ class DummyCls(Telegram): raise Exception('test') -def get_telegram_testobject(mocker, default_conf, mock=True): +def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() if mock: mocker.patch.multiple( @@ -62,8 +62,10 @@ def get_telegram_testobject(mocker, default_conf, mock=True): _init=MagicMock(), _send_msg=msg_mock ) - ftbot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(ftbot) + if not ftbot: + ftbot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(ftbot) + telegram = Telegram(rpc, default_conf) return telegram, ftbot, msg_mock @@ -112,8 +114,10 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None: default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) + rpc = RPC(bot) + dummy = DummyCls(rpc, default_conf) + patch_get_signal(bot, (True, False)) - dummy = DummyCls(bot) dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is True assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) @@ -129,8 +133,10 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) + rpc = RPC(bot) + dummy = DummyCls(rpc, default_conf) + patch_get_signal(bot, (True, False)) - dummy = DummyCls(bot) dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) @@ -144,8 +150,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) + rpc = RPC(bot) + dummy = DummyCls(rpc, default_conf) patch_get_signal(bot, (True, False)) - dummy = DummyCls(bot) dummy.dummy_exception(update=update, context=MagicMock()) assert dummy.state['called'] is False @@ -160,8 +167,10 @@ def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['chat_id'] = "123" status_table = MagicMock() + mocker.patch('freqtrade.rpc.telegram.Telegram._status_table', status_table) + mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', + 'freqtrade.rpc.rpc.RPC', _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', @@ -188,7 +197,6 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'open_order': '(limit buy rem=0.00000000)', 'is_open': True }]), - _status_table=status_table, ) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -642,8 +650,9 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + telegram = Telegram(rpc, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -698,8 +707,9 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + telegram = Telegram(rpc, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -756,8 +766,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) + rpc = RPC(freqtradebot) + telegram = Telegram(rpc, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -1216,8 +1227,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - old_convamount = telegram._fiat_converter.convert_amount - telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 + old_convamount = telegram._rpc._fiat_converter.convert_amount + telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -1274,15 +1285,15 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Profit:* `-57.41%`') # Reset singleton function to avoid random breaks - telegram._fiat_converter.convert_amount = old_convamount + telegram._rpc._fiat_converter.convert_amount = old_convamount def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - old_convamount = telegram._fiat_converter.convert_amount - telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 + old_convamount = telegram._rpc._fiat_converter.convert_amount + telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'exchange': 'Binance', @@ -1303,7 +1314,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') # Reset singleton function to avoid random breaks - telegram._fiat_converter.convert_amount = old_convamount + telegram._rpc._fiat_converter.convert_amount = old_convamount def test_send_msg_status_notification(default_conf, mocker) -> None: @@ -1449,6 +1460,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: bot = MagicMock() bot.send_message = MagicMock() freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtradebot) invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']] default_keys_list = [['/daily', '/profit', '/balance'], @@ -1461,7 +1473,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) def init_telegram(freqtradebot): - telegram = Telegram(freqtradebot) + telegram = Telegram(rpc, default_conf) telegram._updater = MagicMock() telegram._updater.bot = bot return telegram diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 9256a5316..4ca547390 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from requests import RequestException -from freqtrade.rpc import RPCMessageType +from freqtrade.rpc import RPC, RPCMessageType from freqtrade.rpc.webhook import Webhook from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has @@ -45,7 +45,7 @@ def get_webhook_dict() -> dict: def test__init__(mocker, default_conf): default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) assert webhook._config == default_conf @@ -53,7 +53,7 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) # Test buy msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -172,7 +172,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() del default_conf["webhook"]["webhookbuy"] - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", caplog) @@ -181,7 +181,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', @@ -209,7 +209,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): def test__send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() - webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = {'value1': 'DEADBEEF', 'value2': 'ALIVEBEEF', 'value3': 'FREQTRADE'} From 7d2b9447d078969fd7c3ea04f7ba30ea780ce2af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Dec 2020 15:30:55 +0100 Subject: [PATCH 271/281] Update slack link --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b4e8adaf..5c52a8e93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index a9aee342f..1031e4d67 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -169,7 +169,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index dcbaa3ca9..07d686084 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index b424cd31d..5742f512a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -143,7 +143,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index e6882263b..38e040d7a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ For any questions not covered by the documentation or for further information ab Please check out our [discord server](https://discord.gg/MA9v74M). -You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). ## Ready to try? From b3e929d14b1155b3d8de3372d0205c1ade3b49d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:11 +0000 Subject: [PATCH 272/281] Bump mkdocs-material from 6.1.7 to 6.2.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.7 to 6.2.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.7...6.2.3) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2b133cb07..f14afefc1 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.7 +mkdocs-material==6.2.3 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 10840ec170a0f0ad8b2e3e2532500d1c06b3055e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:13 +0000 Subject: [PATCH 273/281] Bump pymdown-extensions from 8.0.1 to 8.1 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.0.1 to 8.1. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.0.1...8.1) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2b133cb07..a7f5a1d47 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ mkdocs-material==6.1.7 mdx_truly_sane_lists==1.2 -pymdown-extensions==8.0.1 +pymdown-extensions==8.1 From 87b896879f4d142eea72936ecf1caf9698bc90f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:13 +0000 Subject: [PATCH 274/281] Bump ccxt from 1.39.52 to 1.39.79 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.52 to 1.39.79. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.52...1.39.79) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c565fee5..c265d6ffe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.5 -ccxt==1.39.52 +ccxt==1.39.79 aiohttp==3.7.3 SQLAlchemy==1.3.22 python-telegram-bot==13.1 From f49260911595f55183f1bdc28ac002f28ee4914f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Dec 2020 05:37:17 +0000 Subject: [PATCH 275/281] Bump blosc from 1.9.2 to 1.10.1 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.9.2 to 1.10.1. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.9.2...v1.10.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c565fee5..a5d4a0979 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ tabulate==0.8.7 pycoingecko==1.4.0 jinja2==2.11.2 tables==3.6.1 -blosc==1.9.2 +blosc==1.10.1 # find first, C search in arrays py_find_1st==1.1.4 From f80ffe279ba45b526ce10cfc147ecb3bf3a9ae3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 09:54:58 +0100 Subject: [PATCH 276/281] Version bump 2020.12 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 3054bc4a1..170f95015 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.11' +__version__ = '2020.12' if __version__ == 'develop': From 8366e67fee163a1d06ce0d48e310ace200dbc410 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 10:19:24 +0100 Subject: [PATCH 277/281] Remove custom header section from docs --- docs/partials/header.html | 54 --------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 docs/partials/header.html diff --git a/docs/partials/header.html b/docs/partials/header.html deleted file mode 100644 index 32202bccc..000000000 --- a/docs/partials/header.html +++ /dev/null @@ -1,54 +0,0 @@ -
- - - - -
From f6e56027b13c583351a8b0af9e49ef80a87aec6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 13:49:08 +0100 Subject: [PATCH 279/281] Reinstate jquery --- docs/overrides/main.html | 8 ++++++++ mkdocs.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 docs/overrides/main.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..916d26770 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block site_meta %} + + + +{% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a7ae0cc96..a0b5d8641 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,7 +38,7 @@ nav: theme: name: material logo: 'images/logo.png' - custom_dir: 'docs' + custom_dir: 'docs/overrides' palette: primary: 'blue grey' accent: 'tear' From a2fdb9d2f6210c89e37c9f1bbbbaf22213e0b42f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 14:02:30 +0100 Subject: [PATCH 280/281] Move jquery to the bottom --- docs/overrides/main.html | 4 ++-- mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 916d26770..910af0973 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,8 +1,8 @@ {% extends "base.html" %} -{% block site_meta %} +{% block scripts %} - {% endblock %} diff --git a/mkdocs.yml b/mkdocs.yml index a0b5d8641..a14c67b03 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ nav: theme: name: material logo: 'images/logo.png' + favicon: 'images/logo.png' custom_dir: 'docs/overrides' palette: primary: 'blue grey' From 0925a3cd19290b075e5ef504ecee041f215ba190 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Dec 2020 14:08:57 +0100 Subject: [PATCH 281/281] Reinstate header partials --- docs/overrides/main.html | 8 ------ docs/partials/header.html | 51 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 3 files changed, 52 insertions(+), 9 deletions(-) delete mode 100644 docs/overrides/main.html create mode 100644 docs/partials/header.html diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index 910af0973..000000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base.html" %} - -{% block scripts %} - - - -{% endblock %} diff --git a/docs/partials/header.html b/docs/partials/header.html new file mode 100644 index 000000000..f5243225b --- /dev/null +++ b/docs/partials/header.html @@ -0,0 +1,51 @@ +{#- +This file was automatically generated - do not edit +-#} +{% set site_url = config.site_url | d(nav.homepage.url, true) | url %} +{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %} +{% set site_url = site_url ~ "/index.html" %} +{% endif %} +
+ + + + +
diff --git a/mkdocs.yml b/mkdocs.yml index a14c67b03..96cfa7651 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,7 +39,7 @@ theme: name: material logo: 'images/logo.png' favicon: 'images/logo.png' - custom_dir: 'docs/overrides' + custom_dir: 'docs' palette: primary: 'blue grey' accent: 'tear'