From 1b4b10f8cdfb2e04f0acba0d6f9f674e8773ecbf Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Tue, 23 Jul 2019 23:45:27 -0500 Subject: [PATCH 001/229] 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/229] 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/229] 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/229] 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/229] 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/229] 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/229] 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/229] 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 13da8f936812bbfd255c4dfce4dd451dc9134c46 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Mon, 9 Nov 2020 08:34:40 +0000 Subject: [PATCH 009/229] Added ConstPairList handler to skip validation of pairs if you want to backtest a pair that's not live any more, e.g. expiring contracts. --- freqtrade/constants.py | 2 +- freqtrade/pairlist/ConstPairList.py | 60 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 freqtrade/pairlist/ConstPairList.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dc5384f6f..21308b2dc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -23,7 +23,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', +AVAILABLE_PAIRLISTS = ['ConstPairList', 'StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] diff --git a/freqtrade/pairlist/ConstPairList.py b/freqtrade/pairlist/ConstPairList.py new file mode 100644 index 000000000..e5b009c55 --- /dev/null +++ b/freqtrade/pairlist/ConstPairList.py @@ -0,0 +1,60 @@ +""" +Const Pair List provider + +Provides pair white list as it configured in config without checking for active markets +""" +import logging +from typing import Any, Dict, List + +from freqtrade.exceptions import OperationalException +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class ConstPairList(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) + + if self._pairlist_pos != 0: + raise OperationalException(f"{self.name} can only be used in the first position " + "in the list of Pairlist Handlers.") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + return f"{self.name}" + + def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + """ + Generate the pairlist + :param cached_pairlist: Previously generated pairlist (cached) + :param tickers: Tickers (from exchange.get_tickers()). + :return: List of pairs + """ + return self._config['exchange']['pair_whitelist'] + + 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 + """ + return pairlist From 916776bb53642b25c4be09bca82ef0c1467798d8 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Mon, 9 Nov 2020 08:37:38 +0000 Subject: [PATCH 010/229] Option to skip exchange validation, required to backtest pairs that are not live on the exchange any more. --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 659ff59bc..baa379db1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -115,7 +115,7 @@ class Exchange: logger.info('Using Exchange "%s"', self.name) - if validate: + if validate and not exchange_config.get('skip_validation'): # Check if timeframe is available self.validate_timeframes(config.get('timeframe')) From 2640dfee9387613480e66da00a4fac5b260c1e41 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Thu, 12 Nov 2020 11:27:30 +0000 Subject: [PATCH 011/229] Revert "Added ConstPairList handler to skip validation of pairs if you want to backtest a pair that's not live any more, e.g. expiring contracts." This reverts commit 13da8f936812bbfd255c4dfce4dd451dc9134c46. --- freqtrade/constants.py | 2 +- freqtrade/pairlist/ConstPairList.py | 60 ----------------------------- 2 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 freqtrade/pairlist/ConstPairList.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 21308b2dc..dc5384f6f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -23,7 +23,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] -AVAILABLE_PAIRLISTS = ['ConstPairList', 'StaticPairList', 'VolumePairList', +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] diff --git a/freqtrade/pairlist/ConstPairList.py b/freqtrade/pairlist/ConstPairList.py deleted file mode 100644 index e5b009c55..000000000 --- a/freqtrade/pairlist/ConstPairList.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Const Pair List provider - -Provides pair white list as it configured in config without checking for active markets -""" -import logging -from typing import Any, Dict, List - -from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList - - -logger = logging.getLogger(__name__) - - -class ConstPairList(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) - - if self._pairlist_pos != 0: - raise OperationalException(f"{self.name} can only be used in the first position " - "in the list of Pairlist Handlers.") - - @property - def needstickers(self) -> bool: - """ - Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed - as tickers argument to filter_pairlist - """ - return False - - def short_desc(self) -> str: - """ - Short whitelist method description - used for startup-messages - -> Please overwrite in subclasses - """ - return f"{self.name}" - - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: - """ - Generate the pairlist - :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). - :return: List of pairs - """ - return self._config['exchange']['pair_whitelist'] - - 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 - """ - return pairlist From 2424ac94c27c146a553aed362c04ac1850733381 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Thu, 12 Nov 2020 11:29:46 +0000 Subject: [PATCH 012/229] skip the check for active markets with flag for existing StaticPairList --- freqtrade/pairlist/StaticPairList.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index aa6268ba3..3b6440763 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -24,6 +24,8 @@ class StaticPairList(IPairList): raise OperationalException(f"{self.name} can only be used in the first position " "in the list of Pairlist Handlers.") + self._allow_inactive = self._pairlistconfig.get('allow_inactive', False) + @property def needstickers(self) -> bool: """ @@ -47,7 +49,10 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers()). :return: List of pairs """ - return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) + if self._allow_inactive: + return self._config['exchange']['pair_whitelist'] + else: + return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ From 2d6bfe1592ab61258d523065cdf4b38cb3857521 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Thu, 12 Nov 2020 11:32:45 +0000 Subject: [PATCH 013/229] only skip pair validation rather than all of it --- freqtrade/exchange/exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index baa379db1..0e982a06f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -115,7 +115,7 @@ class Exchange: logger.info('Using Exchange "%s"', self.name) - if validate and not exchange_config.get('skip_validation'): + if validate: # Check if timeframe is available self.validate_timeframes(config.get('timeframe')) @@ -124,7 +124,8 @@ class Exchange: # Check if all pairs are available self.validate_stakecurrency(config['stake_currency']) - self.validate_pairs(config['exchange']['pair_whitelist']) + if not exchange_config.get('skip_pair_validation'): + self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0)) From 26176d4c91c20cb24a762d4eb8ab204a9951d1ac Mon Sep 17 00:00:00 2001 From: Samaoo Date: Sun, 15 Nov 2020 19:55:09 +0100 Subject: [PATCH 014/229] Update exchanges.md According to https://blog.kraken.com/post/5282/stop-loss-limit-take-profit-limit-two-new-advanced-orders-go-live-on-kraken/ Stop Loss Limit orders are enabled again --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index fcf7c1cad..ac386c937 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled. + Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it. ### Historic Kraken data From ef4ab601a9fc2a059d0eaf3fd77c4bb7f63f8ac3 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Sun, 15 Nov 2020 20:02:19 +0100 Subject: [PATCH 015/229] Update exchanges.md --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index ac386c937..5d7505795 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it. + Kraken supports `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it. ### Historic Kraken data From 4a215821cdc2c3017c15fff2082a2b5e58773c61 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 17 Nov 2020 14:07:24 +0100 Subject: [PATCH 016/229] Fix typo in windows installation docs --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 924459a6d..0c1eeef64 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -50,7 +50,7 @@ freqtrade error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools ``` -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. +Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. 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. From 854d0c481f0b7569f4e28418a64e088aeb6c0bd4 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 17 Nov 2020 14:14:42 +0100 Subject: [PATCH 017/229] Update windows_installation.md --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 0c1eeef64..5341ce96b 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -52,6 +52,6 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -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. +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. --- From 52c9a2c37f62196d43f09b41fb0a90dc187a37e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 07:30:28 +0100 Subject: [PATCH 018/229] Convert np to None when loading hdf5 trades to allow duplicate detection --- freqtrade/data/history/hdf5datahandler.py | 4 +++- tests/data/test_history.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index f6cf9e0d9..00e41673d 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -3,6 +3,7 @@ import re from pathlib import Path from typing import List, Optional +import numpy as np import pandas as pd from freqtrade import misc @@ -175,7 +176,8 @@ class HDF5DataHandler(IDataHandler): if timerange.stoptype == 'date': where.append(f"timestamp < {timerange.stopts * 1e3}") - trades = pd.read_hdf(filename, key=key, mode="r", where=where) + trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where) + trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None}) return trades.values.tolist() def trades_purge(self, pair: str) -> bool: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index bbc6e55b4..538a0840f 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -724,6 +724,8 @@ def test_hdf5datahandler_trades_load(testdatadir): trades2 = dh._trades_load('XRP/ETH', timerange) assert len(trades) > len(trades2) + # Check that ID is None (If it's nan, it's wrong) + assert trades2[0][2] is None # unfiltered load has trades before starttime assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0 From f88fe5d950daf3ab68f9153648bf8d94f20593b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 19:14:43 +0100 Subject: [PATCH 019/229] Document new "allow_inactive" option --- docs/includes/pairlists.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ae4ec818d..aebd084ab 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -35,6 +35,10 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis ], ``` +By default, only currently enabled pairs are allowed. +To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration. +This can be useful for backtesting expired pairs (like quarterly spot-markets). + #### Volume Pair List `VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). From 97e58a42f4caf1803af411937123461bda7ca244 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 19:17:31 +0100 Subject: [PATCH 020/229] Update documentation with new options --- docs/configuration.md | 1 + docs/includes/pairlists.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 47362e525..080ddd046 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -87,6 +87,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean | `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 diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index aebd084ab..e6a9fc1a8 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -38,6 +38,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis By default, only currently enabled pairs are allowed. To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration. This can be useful for backtesting expired pairs (like quarterly spot-markets). +This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration. #### Volume Pair List From aa0c3dced8bac13b35e241f287d1d9a4cbcecd31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 20 Nov 2020 13:14:02 +0100 Subject: [PATCH 021/229] Improve order types documentation --- docs/configuration.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 080ddd046..56ba13414 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -314,22 +314,21 @@ Configuration: } ``` -!!! Note +!!! Note "Market order support" Not all exchanges support "market" orders. The following message will be shown if your exchange does not support market orders: - `"Exchange does not support market orders."` + `"Exchange does not support market orders."` and the bot will refuse to start. -!!! Note - Stoploss on exchange interval is not mandatory. Do not change its value if you are +!!! Warning "Using market orders" + Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders. + +!!! Note "Stoploss on exchange" + `stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are unsure of what you are doing. For more information about how stoploss works please refer to [the stoploss documentation](stoploss.md). -!!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. -!!! Warning "Using market orders" - Please read the section [Market order pricing](#market-order-pricing) section when using market orders. - !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. From 5ed85963a950eada9571b2f6f19130ff15878157 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 10:39:49 +0100 Subject: [PATCH 022/229] Allow forcebuy price to be a string by converting it to float fix #3970 --- freqtrade/rpc/api_server.py | 2 ++ freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 7f4773d57..384d7c6c2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -508,6 +508,8 @@ class ApiServer(RPC): """ asset = request.json.get("pair") price = request.json.get("price", None) + price = float(price) if price is not None else price + trade = self._rpc_forcebuy(asset, price) if trade: return jsonify(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 90564a19d..e608a2274 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -524,7 +524,7 @@ class RPC: stake_currency = self._freqtrade.config.get('stake_currency') if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: raise RPCException( - f'Wrong pair selected. Please pairs with stake {stake_currency} pairs only') + f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') # check if valid pair # check if pair already has an open pair diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 23ca53e53..47e0f763d 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -868,7 +868,8 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> assert trade.open_rate == 0.0001 # Test buy pair not with stakes - with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'): + with pytest.raises(RPCException, + match=r'Wrong pair selected. Only pairs with stake-currency.*'): rpc._rpc_forcebuy('LTC/ETH', 0.0001) pair = 'XRP/BTC' From 83861fabdea798100c5322a9efe5db4f7a23c84a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 10:52:15 +0100 Subject: [PATCH 023/229] Fix #3967, move TradeList type to constants --- freqtrade/constants.py | 3 +++ freqtrade/data/converter.py | 10 +++++---- freqtrade/data/history/hdf5datahandler.py | 4 ++-- freqtrade/data/history/idatahandler.py | 5 +---- freqtrade/data/history/jsondatahandler.py | 4 ++-- tests/data/test_converter.py | 27 ++++++++++++++++++++++- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dc5384f6f..3271dda39 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -365,3 +365,6 @@ CANCEL_REASON = { # List of pairs with their timeframes PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] + +# Type for trades list +TradeList = List[List] diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 38fa670e9..09930950a 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList logger = logging.getLogger(__name__) @@ -168,7 +168,7 @@ def trades_remove_duplicates(trades: List[List]) -> List[List]: return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))] -def trades_dict_to_list(trades: List[Dict]) -> List[List]: +def trades_dict_to_list(trades: List[Dict]) -> TradeList: """ Convert fetch_trades result into a List (to be more memory efficient). :param trades: List of trades, as returned by ccxt.fetch_trades. @@ -177,16 +177,18 @@ def trades_dict_to_list(trades: List[Dict]) -> List[List]: return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades] -def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame: +def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame: """ Converts trades list to OHLCV list - TODO: This should get a dedicated test :param trades: List of trades, as returned by ccxt.fetch_trades. :param timeframe: Timeframe to resample data to :return: OHLCV Dataframe. + :raises: Valueerror if no trades are provided """ from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) + if not trades: + raise ValueError('Trade-list empty.') df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True,) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 00e41673d..d116637e7 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -9,9 +9,9 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, - ListPairsWithTimeframes) + ListPairsWithTimeframes, TradeList) -from .idatahandler import IDataHandler, TradeList +from .idatahandler import IDataHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index a170a9dc5..070d9039d 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -13,16 +13,13 @@ from typing import List, Optional, Type from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import ListPairsWithTimeframes, TradeList from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe from freqtrade.exchange import timeframe_to_seconds logger = logging.getLogger(__name__) -# Type for trades list -TradeList = List[List] - class IDataHandler(ABC): diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 6436aa13d..9122170d5 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,10 +8,10 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList from freqtrade.data.converter import trades_dict_to_list -from .idatahandler import IDataHandler, TradeList +from .idatahandler import IDataHandler logger = logging.getLogger(__name__) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index fdba7900f..4fdcce4d2 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -1,10 +1,13 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +import pytest + from freqtrade.configuration.timerange import TimeRange from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, ohlcv_fill_up_missing_data, ohlcv_to_dataframe, - trades_dict_to_list, trades_remove_duplicates, trim_dataframe) + trades_dict_to_list, trades_remove_duplicates, + trades_to_ohlcv, trim_dataframe) from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) from tests.conftest import log_has @@ -26,6 +29,28 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog): assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog) +def test_trades_to_ohlcv(ohlcv_history_list, caplog): + + caplog.set_level(logging.DEBUG) + with pytest.raises(ValueError, match="Trade-list empty."): + trades_to_ohlcv([], '1m') + + trades = [ + [1570752011620, "13519807", None, "sell", 0.00141342, 23.0, 0.03250866], + [1570752011620, "13519808", None, "sell", 0.00141266, 54.0, 0.07628364], + [1570752017964, "13519809", None, "sell", 0.00141266, 8.0, 0.01130128]] + + df = trades_to_ohlcv(trades, '1m') + assert not df.empty + assert len(df) == 1 + assert 'open' in df.columns + assert 'high' in df.columns + assert 'low' in df.columns + assert 'close' in df.columns + assert df.loc[:, 'high'][0] == 0.00141342 + assert df.loc[:, 'low'][0] == 0.00141266 + + def test_ohlcv_fill_up_missing_data(testdatadir, caplog): data = load_pair_history(datadir=testdatadir, timeframe='1m', From e8e3ca0c3c114637a460fdddeea5fca155bdf534 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 10:57:19 +0100 Subject: [PATCH 024/229] Catch ValueError from trade_conversion closes #3967 --- freqtrade/data/converter.py | 2 +- freqtrade/data/history/history_utils.py | 9 ++++++--- tests/data/test_history.py | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 09930950a..d4053abaa 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -183,7 +183,7 @@ def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame: :param trades: List of trades, as returned by ccxt.fetch_trades. :param timeframe: Timeframe to resample data to :return: OHLCV Dataframe. - :raises: Valueerror if no trades are provided + :raises: ValueError if no trades are provided """ from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index a420b9dcc..17b510b92 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -356,9 +356,12 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], if erase: if data_handler_ohlcv.ohlcv_purge(pair, timeframe): logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.') - ohlcv = trades_to_ohlcv(trades, timeframe) - # Store ohlcv - data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv) + try: + ohlcv = trades_to_ohlcv(trades, timeframe) + # Store ohlcv + data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv) + except ValueError: + logger.exception(f'Could not convert {pair} to OHLCV.') def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 538a0840f..905798041 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -620,6 +620,12 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): _clean_test_file(file1) _clean_test_file(file5) + assert not log_has('Could not convert NoDatapair to OHLCV.', caplog) + + convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], + datadir=testdatadir, timerange=tr, erase=True) + assert log_has('Could not convert NoDatapair to OHLCV.', caplog) + def test_datahandler_ohlcv_get_pairs(testdatadir): pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m') From 89ea8dbef2a73c050649d45db6185de8fb8cf789 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 11:13:44 +0100 Subject: [PATCH 025/229] Update slack invite --- CONTRIBUTING.md | 3 +-- README.md | 2 +- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- docs/strategy-customization.md | 2 -- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 399588f88..6b4e8adaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,8 +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 [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) -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-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index c9f4d0a52..4daa1854a 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,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 [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). 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-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. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index 8ef816d5d..c253f4460 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 in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) 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-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index a775060de..aa33218fb 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -145,7 +145,7 @@ freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDa ### 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/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - 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-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. * 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 5608587db..c6697d165 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,7 +63,7 @@ Alternatively For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. +Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) to join the Freqtrade Slack channel. Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6c7d78864..db007985f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -770,8 +770,6 @@ To get additional Ideas for strategies, head over to our [strategy repository](h Feel free to use any of them as inspiration for your own strategies. We're happy to accept Pull Requests containing new Strategies to that repo. -We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) which is a great place to get and/or share ideas. - ## Next step Now you have a perfect strategy you probably want to backtest it. From 4d60a4cf4e9a850a1c03ac2923e163b6969564fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 11:32:46 +0100 Subject: [PATCH 026/229] Add warning to StochRSI in sample strategy closes #2961 --- freqtrade/templates/sample_strategy.py | 2 ++ freqtrade/templates/subtemplates/indicators_full.j2 | 2 ++ 2 files changed, 4 insertions(+) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 44590dbbe..b3f9fef07 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -184,6 +184,8 @@ class SampleStrategy(IStrategy): dataframe['fastk'] = stoch_fast['fastk'] # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. # stoch_rsi = ta.STOCHRSI(dataframe) # dataframe['fastd_rsi'] = stoch_rsi['fastd'] # dataframe['fastk_rsi'] = stoch_rsi['fastk'] diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 index 60a358bec..57d2ca665 100644 --- a/freqtrade/templates/subtemplates/indicators_full.j2 +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -62,6 +62,8 @@ dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] # # Stochastic RSI +# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. +# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. # stoch_rsi = ta.STOCHRSI(dataframe) # dataframe['fastd_rsi'] = stoch_rsi['fastd'] # dataframe['fastk_rsi'] = stoch_rsi['fastk'] From 73f0e6e704fc85c57f0b7d982a1ff0df0fae7fa5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 11:40:28 +0100 Subject: [PATCH 027/229] Improve wording for discord server fix link to correct docker install guide --- README.md | 10 ++++------ docs/index.md | 13 +++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4daa1854a..8526b5c91 100644 --- a/README.md +++ b/README.md @@ -132,15 +132,13 @@ The project is currently setup in two main branches: ## Support -### Help / Slack / Discord +### Help / Discord / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). +Please check out our [discord server](https://discord.gg/MA9v74M). -Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). - -*Note*: Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) diff --git a/docs/index.md b/docs/index.md index c6697d165..f63aeb6b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,17 +59,14 @@ Alternatively ## Support -### Help / Slack / Discord +### Help / Discord / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) to join the Freqtrade Slack channel. +Please check out our [discord server](https://discord.gg/MA9v74M). -Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). - -!!! Note - Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). ## Ready to try? -Begin by reading our installation guide [for docker](docker.md) (recommended), or for [installation without docker](installation.md). +Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md). From fb86d8f8ff43646ded784c1ebace16cc1e8fd616 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:28:50 +0100 Subject: [PATCH 028/229] Add get_historic_ohlcv_as_df to support VolatilityFilter --- freqtrade/exchange/exchange.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2bbdb0d59..2f52c512f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -679,12 +679,25 @@ class Exchange: :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from - :returns List with candle (OHLCV) data + :return: List with candle (OHLCV) data """ return asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms)) + def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, + since_ms: int) -> DataFrame: + """ + Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe + :param pair: Pair to download + :param timeframe: Timeframe to get data for + :param since_ms: Timestamp in milliseconds to get history from + :return: OHLCV DataFrame + """ + ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms) + return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: From 109824c9a80cb78c7c4ec9d6f90cb1c8c3afa640 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:29:11 +0100 Subject: [PATCH 029/229] Add VolatilityFilter --- freqtrade/constants.py | 2 +- freqtrade/pairlist/AgeFilter.py | 2 +- freqtrade/pairlist/volatilityfilter.py | 89 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 freqtrade/pairlist/volatilityfilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3271dda39..55d802587 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', 'VolatilityFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 19cf1c090..352fff082 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -49,7 +49,7 @@ 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, ticker: Dict) -> bool: """ Validate age for the ticker :param ticker: ticker dict as returned from ccxt.load_markets() diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py new file mode 100644 index 000000000..6039f6f69 --- /dev/null +++ b/freqtrade/pairlist/volatilityfilter.py @@ -0,0 +1,89 @@ +""" +Minimum age (days listed) pair list filter +""" +import logging +from typing import Any, Dict + +import arrow +from cachetools.ttl import TTLCache + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(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) + + self._days = pairlistconfig.get('volatility_over_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0.01) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires min_days_listed to be >= 1") + if self._days > exchange.ohlcv_candle_limit: + raise OperationalException("VolatilityFilter requires min_days_listed to not exceed " + "exchange max request size " + f"({exchange.ohlcv_candle_limit})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with volatility below {self._min_volatility} " + f"over the last {plural(self._days, 'day')}.") + + def _validate_pair(self, ticker: Dict) -> bool: + """ + Validate volatility + :param ticker: ticker dict as returned from ccxt.load_markets() + :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] + + 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: + highest_high = daily_candles['high'].max() + lowest_low = daily_candles['low'].min() + pct_change = (highest_high - lowest_low) / lowest_low + if pct_change >= self._min_volatility: + result = True + else: + self.log_on_refresh(logger.info, + f"Removed {pair} from whitelist, " + f"because volatility over {plural(self._days, 'day')} is " + f"{pct_change:.3f}, which is below the " + f"threshold of {self._min_volatility}.") + result = False + self._pair_cache[pair] = result + + return result From 191616e4e5cbb558f48ec39e67bf5399fbf87da5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:39:04 +0100 Subject: [PATCH 030/229] Add first tests for volatilityFilter --- tests/pairlist/test_pairlist.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f05bef1e..3e1cca30c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -340,6 +340,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), + ([{"method": "StaticPairList"}, + {"method": "VolatilityFilter", "volatility_over_days": 10, + "min_volatility": 0.01, "refresh_period": 1440}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history_list, pairlists, base_currency, @@ -617,6 +621,11 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected + ({"method": "VolatilityFilter", "volatility_over_days": 10, "min_volatility": 0.01}, + "[{'VolatilityFilter': 'VolatilityFilter - Filtering pairs with volatility below 0.01 " + "over the last days.'}]", + None + ), ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): From 6b672cd0b95f8a35fe83dab95e6b931e6b85c51d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:43:29 +0100 Subject: [PATCH 031/229] Document volatilityFilter --- docs/includes/pairlists.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index e6a9fc1a8..301a5453d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -19,6 +19,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) +* [`VolatilityFilter`](#volatilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -118,6 +119,27 @@ Example: If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. +#### VolatilityFilter + +Removes pairs where the difference between lowest low and highest high over `volatility_over_days` days is below `min_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. + +In the below example: +If volatility over the last 10 days is <1%, remove the pair from the whitelist. + +```json +"pairlists": [ + { + "method": "VolatilityFilter", + "volatility_over_days": 10, + "min_volatility": 0.01, + "refresh_period": 1440 + } +] +``` + +!!! Tip + This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely hard to trade with profit. + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. @@ -137,6 +159,12 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + { + "method": "VolatilityFilter", + "volatility_over_days": 10, + "min_volatility": 0.01, + "refresh_period": 1440 + }, {"method": "ShuffleFilter", "seed": 42} ], ``` From f8fab5c4f8d120b7838cac24c6a0c7d30df85fc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:51:39 +0100 Subject: [PATCH 032/229] Add tests for failure cases --- freqtrade/pairlist/volatilityfilter.py | 4 ++-- tests/pairlist/test_pairlist.py | 33 ++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py index 6039f6f69..e9ca61794 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/volatilityfilter.py @@ -29,9 +29,9 @@ class VolatilityFilter(IPairList): self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) if self._days < 1: - raise OperationalException("VolatilityFilter requires min_days_listed to be >= 1") + raise OperationalException("VolatilityFilter requires volatility_over_days to be >= 1") if self._days > exchange.ohlcv_candle_limit: - raise OperationalException("VolatilityFilter requires min_days_listed to not exceed " + raise OperationalException("VolatilityFilter requires volatility_over_days to not exceed " "exchange max request size " f"({exchange.ohlcv_candle_limit})") diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 3e1cca30c..5bbb233b4 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -58,7 +58,7 @@ def whitelist_conf_2(default_conf): @pytest.fixture(scope="function") -def whitelist_conf_3(default_conf): +def whitelist_conf_agefilter(default_conf): default_conf['stake_currency'] = 'BTC' default_conf['exchange']['pair_whitelist'] = [ 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', @@ -532,7 +532,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}] @@ -547,7 +547,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}] @@ -563,7 +563,7 @@ 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_3, tickers, ohlcv_history_list): +def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -575,7 +575,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 @@ -586,6 +586,29 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count +def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'VolatilityFilter', 'volatility_over_days': 99999}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + with pytest.raises(OperationalException, + match=r'VolatilityFilter requires volatility_over_days to not exceed ' + r'exchange max request size \([0-9]+\)'): + get_patched_freqtradebot(mocker, default_conf) + + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'VolatilityFilter', 'volatility_over_days': 0}] + + with pytest.raises(OperationalException, + match='VolatilityFilter requires volatility_over_days to be >= 1'): + get_patched_freqtradebot(mocker, default_conf) + + @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, "max_price": 1.0}, From 2e1551a2ebce9cd9d288ba03a019778ff758b7ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 16:01:52 +0100 Subject: [PATCH 033/229] Improve tests of volatilityfilter --- docs/includes/pairlists.md | 2 +- freqtrade/pairlist/volatilityfilter.py | 4 ++-- tests/pairlist/test_pairlist.py | 33 ++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 301a5453d..7cd2369b1 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -138,7 +138,7 @@ If volatility over the last 10 days is <1%, remove the pair from the whitelist. ``` !!! Tip - This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely hard to trade with profit. + This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely difficult to trade with profit. ### Full example of Pairlist Handlers diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py index e9ca61794..415b6e89e 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/volatilityfilter.py @@ -31,8 +31,8 @@ class VolatilityFilter(IPairList): if self._days < 1: raise OperationalException("VolatilityFilter requires volatility_over_days to be >= 1") if self._days > exchange.ohlcv_candle_limit: - raise OperationalException("VolatilityFilter requires volatility_over_days to not exceed " - "exchange max request size " + raise OperationalException("VolatilityFilter requires volatility_over_days to not " + "exceed exchange max request size " f"({exchange.ohlcv_candle_limit})") @property diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5bbb233b4..e9df5d3f4 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -609,6 +609,39 @@ def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): get_patched_freqtradebot(mocker, default_conf) +@pytest.mark.parametrize('min_volatility,expected_length', [ + (0.01, 5), + (0.05, 0), # Setting volatility to 5% removes all pairs from the whitelist. +]) +def test_volatilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, + min_volatility, expected_length): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'VolatilityFilter', 'volatility_over_days': 2, + 'min_volatility': min_volatility}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == expected_length + assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + + previous_call_count = freqtrade.exchange.get_historic_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 + + @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, "max_price": 1.0}, From f12a8afd4151d6a2f069f5375291dc57e6b862b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 10:56:19 +0100 Subject: [PATCH 034/229] Add test for ohlcv_as_df --- tests/exchange/test_exchange.py | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e4452a83c..42681b367 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1307,6 +1307,57 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): assert log_has_re(r"Async code raised an exception: .*", caplog) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + ohlcv = [ + [ + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + pair = 'ETH/BTC' + + async def mock_candle_hist(pair, timeframe, since_ms): + return pair, timeframe, ohlcv + + exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) + # one_call calculation * 1.8 should do 2 calls + + since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 + ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int(( + arrow.utcnow().int_timestamp - since) * 1000)) + + assert exchange._async_get_candle_history.call_count == 2 + # Returns twice the above OHLCV data + assert len(ret) == 2 + assert isinstance(ret, DataFrame) + assert 'date' in ret.columns + assert 'open' in ret.columns + assert 'close' in ret.columns + assert 'high' in ret.columns + + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ [ From 7e4fe23bf94128fa1df7477011d65d6d3ff2afd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 11:08:01 +0100 Subject: [PATCH 035/229] Add VolatilityFilter to full config --- config_full.json.example | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 45c5c695c..0d82b9a2b 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -67,7 +67,13 @@ {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005} + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + { + "method": "VolatilityFilter", + "volatility_over_days": 10, + "min_volatility": 0.01, + "refresh_period": 1440 + } ], "exchange": { "name": "bittrex", From 29c6a9263de13b3a480662d1c59b203512df8bd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 15:50:44 +0100 Subject: [PATCH 036/229] Protect against 0 values --- freqtrade/pairlist/volatilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py index 415b6e89e..14ac0c617 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/volatilityfilter.py @@ -74,7 +74,7 @@ class VolatilityFilter(IPairList): if daily_candles is not None: highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() - pct_change = (highest_high - lowest_low) / lowest_low + pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 if pct_change >= self._min_volatility: result = True else: From ec330112552afc3902bef12b6ea3884a39a16193 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:43 +0000 Subject: [PATCH 037/229] Bump ccxt from 1.37.69 to 1.38.13 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.37.69 to 1.38.13. - [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.37.69...1.38.13) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67b69a5dd..7f2ac98b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.69 +ccxt==1.38.13 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 83b4cd7b39f1ea2eba4d820043f47b7a42c96fd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:51 +0000 Subject: [PATCH 038/229] Bump mkdocs-material from 6.1.5 to 6.1.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.5 to 6.1.6. - [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.5...6.1.6) 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 cd294bef6..87bc6dfdd 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.5 +mkdocs-material==6.1.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From be4807d85c24c719f87a3ee43d13012d5988de41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:52 +0000 Subject: [PATCH 039/229] Bump questionary from 1.8.0 to 1.8.1 Bumps [questionary](https://github.com/tmbo/questionary) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.8.0...1.8.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 67b69a5dd..66fa04824 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.0 +questionary==1.8.1 prompt-toolkit==3.0.8 From 7c7a8190abf5548ba54cc54141929668492ac56b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:54 +0000 Subject: [PATCH 040/229] Bump python-rapidjson from 0.9.3 to 0.9.4 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.9.3 to 0.9.4. - [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.3...v0.9.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67b69a5dd..487b19159 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.3 +python-rapidjson==0.9.4 # Notify systemd sdnotify==0.3.2 From 56629d882e1d5fc39c00de14f5995af9ce16ce6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:51:03 +0000 Subject: [PATCH 041/229] Bump coveralls from 2.1.2 to 2.2.0 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 2.1.2 to 2.2.0. - [Release notes](https://github.com/coveralls-clients/coveralls-python/releases) - [Changelog](https://github.com/coveralls-clients/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/coveralls-clients/coveralls-python/compare/2.1.2...2.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 1c96a880a..e681274c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.1.2 +coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 From 1ec99e6b76010c0f0471d009b64e7f3bef0c433d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 08:05:45 +0000 Subject: [PATCH 042/229] Bump aiohttp from 3.7.2 to 3.7.3 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.2 to 3.7.3. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.2...v3.7.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa3024e30..7490688d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.19.4 pandas==1.1.4 ccxt==1.38.13 -aiohttp==3.7.2 +aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 From 312533fded2c794798a860b3ad523aec6f10ecc1 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Mon, 23 Nov 2020 22:08:53 -0600 Subject: [PATCH 043/229] 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 730c9ce4719ee257b62a149d2c807c5da43e07d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 06:57:11 +0100 Subject: [PATCH 044/229] Add Max_open_trades to summary metrics --- docs/backtesting.md | 9 +++++++-- freqtrade/optimize/optimize_reports.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 84911568b..277b11083 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -162,6 +162,8 @@ A backtesting result will look like that: |-----------------------+---------------------| | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | @@ -233,6 +235,8 @@ It contains some useful key metrics about performance of your strategy on backte |-----------------------+---------------------| | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | @@ -251,16 +255,17 @@ 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. -- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `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. - `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). -- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). +- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. ### Assumptions made by backtesting diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c977a991b..fc04cbd93 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -396,6 +396,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: 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']), From 006436a18d2d2c821ca4a51ff1e604074ddacf9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 07:47:35 +0100 Subject: [PATCH 045/229] Require use_sell_signal to be true for edge Otherwise edge will have strange results, as edge runs with sell signal, while the bot runs without sell signal, causing results to be invalid closes #3900 --- freqtrade/configuration/config_validation.py | 4 ++++ tests/test_configuration.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index d4612d8e0..ab21bc686 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -137,6 +137,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None: "Edge and VolumePairList are incompatible, " "Edge will override whatever pairs VolumePairlist selects." ) + if not conf.get('ask_strategy', {}).get('use_sell_signal', True): + raise OperationalException( + "Edge requires `use_sell_signal` to be True, otherwise no sells will happen." + ) def _validate_whitelist(conf: Dict[str, Any]) -> None: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 7d6c81f74..9594b6413 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -812,6 +812,21 @@ def test_validate_edge(edge_conf): validate_config_consistency(edge_conf) +def test_validate_edge2(edge_conf): + edge_conf.update({"ask_strategy": { + "use_sell_signal": True, + }}) + # Passes test + validate_config_consistency(edge_conf) + + edge_conf.update({"ask_strategy": { + "use_sell_signal": False, + }}) + with pytest.raises(OperationalException, match="Edge requires `use_sell_signal` to be True, " + "otherwise no sells will happen."): + validate_config_consistency(edge_conf) + + def test_validate_whitelist(default_conf): default_conf['runmode'] = RunMode.DRY_RUN # Test regular case - has whitelist and uses StaticPairlist From bd98ff6332f9bd3d4ea73d1ee18446b48f0e187e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 20:24:51 +0100 Subject: [PATCH 046/229] Update docstring in all pairlists --- freqtrade/pairlist/AgeFilter.py | 2 +- freqtrade/pairlist/IPairList.py | 2 +- freqtrade/pairlist/PrecisionFilter.py | 2 +- freqtrade/pairlist/PriceFilter.py | 2 +- freqtrade/pairlist/ShuffleFilter.py | 2 +- freqtrade/pairlist/SpreadFilter.py | 2 +- freqtrade/pairlist/StaticPairList.py | 2 +- freqtrade/pairlist/VolumePairList.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 19cf1c090..20635a9ed 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -37,7 +37,7 @@ class AgeFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 6b5bd11e7..c869e499b 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(ABC): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index cf853397b..29e32fd44 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -32,7 +32,7 @@ class PrecisionFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 8cd57ee1d..bef1c0a15 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -35,7 +35,7 @@ class PriceFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/pairlist/ShuffleFilter.py index eb4f6dcc3..28778db7b 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/pairlist/ShuffleFilter.py @@ -25,7 +25,7 @@ class ShuffleFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 2527a3131..a636b90bd 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -24,7 +24,7 @@ class SpreadFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 3b6440763..2879cb364 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -30,7 +30,7 @@ class StaticPairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 44e5c52d7..7d3c2c653 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -49,7 +49,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True From ceb50a78071c7295b486de887161875126bb3f72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 07:57:23 +0100 Subject: [PATCH 047/229] use exception handler when downloading data closes #3992 --- freqtrade/data/history/history_utils.py | 12 +++++------- tests/data/test_history.py | 5 +---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 17b510b92..3b8b5a2f0 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -214,10 +214,9 @@ def _download_pair_history(datadir: Path, data_handler.ohlcv_store(pair, timeframe, data=data) return True - except Exception as e: - logger.error( - f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. ' - f'Error: {e}' + except Exception: + logger.exception( + f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}.' ) return False @@ -304,10 +303,9 @@ def _download_trades_history(exchange: Exchange, logger.info(f"New Amount of trades: {len(trades)}") return True - except Exception as e: - logger.error( + except Exception: + logger.exception( f'Failed to download historic trades for pair: "{pair}". ' - f'Error: {e}' ) return False diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 905798041..99b22adda 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -312,10 +312,7 @@ def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog, # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) - assert log_has( - 'Failed to download history data for pair: "MEME/BTC", timeframe: 1m. ' - 'Error: File Error', caplog - ) + assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog) def test_load_partial_missing(testdatadir, caplog) -> None: From 99b67348b206a75556d35f3994ec293b3d543dac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 14:30:58 +0100 Subject: [PATCH 048/229] Add test for double-logging --- tests/test_configuration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 9594b6413..47d393860 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -678,6 +678,9 @@ def test_set_loggers_syslog(mocker): assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] assert [x for x in logger.handlers if type(x) == logging.StreamHandler] assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler] + # setting up logging again should NOT cause the loggers to be added a second time. + setup_logging(config) + assert len(logger.handlers) == 3 # reset handlers to not break pytest logger.handlers = orig_handlers From 0104c9fde68601e694969006029cd1b17e8c015c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 14:31:34 +0100 Subject: [PATCH 049/229] Fix double logging --- freqtrade/loggers.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 169cd2610..fbb05d879 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -37,6 +37,13 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None: ) +def get_existing_handlers(handlertype): + """ + Returns Existing handler or None (if the handler has not yet been added to the root handlers). + """ + return next((h for h in logging.root.handlers if isinstance(h, handlertype)), None) + + def setup_logging_pre() -> None: """ Early setup for logging. @@ -71,18 +78,24 @@ def setup_logging(config: Dict[str, Any]) -> None: # config['logfilename']), which defaults to '/dev/log', applicable for most # of the systems. address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else '/dev/log' - handler = SysLogHandler(address=address) + handler_sl = get_existing_handlers(SysLogHandler) + if handler_sl: + logging.root.removeHandler(handler_sl) + handler_sl = SysLogHandler(address=address) # No datetime field for logging into syslog, to allow syslog # to perform reduction of repeating messages if this is set in the # syslog config. The messages should be equal for this. - handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) - logging.root.addHandler(handler) + handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) + logging.root.addHandler(handler_sl) elif s[0] == 'journald': try: from systemd.journal import JournaldLogHandler except ImportError: raise OperationalException("You need the systemd python package be installed in " "order to use logging to journald.") + handler_jd = get_existing_handlers(JournaldLogHandler) + if handler_jd: + logging.root.removeHandler(handler_jd) handler_jd = JournaldLogHandler() # No datetime field for logging into journald, to allow syslog # to perform reduction of repeating messages if this is set in the @@ -90,6 +103,9 @@ def setup_logging(config: Dict[str, Any]) -> None: handler_jd.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) logging.root.addHandler(handler_jd) else: + handler_rf = get_existing_handlers(RotatingFileHandler) + if handler_rf: + logging.root.removeHandler(handler_rf) handler_rf = RotatingFileHandler(logfile, maxBytes=1024 * 1024 * 10, # 10Mb backupCount=10) From b9980330a5469aa99160c8e40815a0c8707e0482 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 14:53:25 +0100 Subject: [PATCH 050/229] Add explicit test for FileHandler --- tests/test_configuration.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 47d393860..3501f1f3d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -663,7 +663,7 @@ def test_set_loggers() -> None: @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_set_loggers_syslog(mocker): +def test_set_loggers_syslog(): logger = logging.getLogger() orig_handlers = logger.handlers logger.handlers = [] @@ -685,6 +685,30 @@ def test_set_loggers_syslog(mocker): logger.handlers = orig_handlers +def test_set_loggers_Filehandler(tmpdir): + logger = logging.getLogger() + orig_handlers = logger.handlers + logger.handlers = [] + logfile = Path(tmpdir) / 'ft_logfile.log' + config = {'verbosity': 2, + 'logfile': str(logfile), + } + + setup_logging_pre() + setup_logging(config) + assert len(logger.handlers) == 3 + assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler] + assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler] + # setting up logging again should NOT cause the loggers to be added a second time. + setup_logging(config) + assert len(logger.handlers) == 3 + # reset handlers to not break pytest + if logfile.exists: + logfile.unlink() + logger.handlers = orig_handlers + + @pytest.mark.skip(reason="systemd is not installed on every system, so we're not testing this.") def test_set_loggers_journald(mocker): logger = logging.getLogger() From 46389e343bc0314427823023604c55bf212686e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 15:10:17 +0100 Subject: [PATCH 051/229] Skip filehandler test on windows - as that causes a permission-error --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 3501f1f3d..e6c91a96e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -685,6 +685,7 @@ def test_set_loggers_syslog(): logger.handlers = orig_handlers +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") def test_set_loggers_Filehandler(tmpdir): logger = logging.getLogger() orig_handlers = logger.handlers From 8f1d2ff0701bcf34609cec0c52e25697e3a8c65f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 19:47:27 +0100 Subject: [PATCH 052/229] Renamd volatilityFilter to RangeStabilityFilter --- config_full.json.example | 4 +-- docs/includes/pairlists.md | 22 ++++++------- freqtrade/constants.py | 2 +- ...ilityfilter.py => rangestabilityfilter.py} | 22 ++++++------- tests/pairlist/test_pairlist.py | 32 +++++++++---------- 5 files changed, 41 insertions(+), 41 deletions(-) rename freqtrade/pairlist/{volatilityfilter.py => rangestabilityfilter.py} (77%) diff --git a/config_full.json.example b/config_full.json.example index 0d82b9a2b..365b6180b 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -69,8 +69,8 @@ {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, { - "method": "VolatilityFilter", - "volatility_over_days": 10, + "method": "RangeStabilityFilter", + "lookback_days": 10, "min_volatility": 0.01, "refresh_period": 1440 } diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 7cd2369b1..149e784bd 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -19,7 +19,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) -* [`VolatilityFilter`](#volatilityfilter) +* [`RangeStabilityFilter`](#rangestabilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -119,26 +119,26 @@ Example: If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. -#### VolatilityFilter +#### RangeStabilityFilter -Removes pairs where the difference between lowest low and highest high over `volatility_over_days` days is below `min_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. +Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. In the below example: -If volatility over the last 10 days is <1%, remove the pair from the whitelist. +If the trading range over the last 10 days is <1%, remove the pair from the whitelist. ```json "pairlists": [ { - "method": "VolatilityFilter", - "volatility_over_days": 10, - "min_volatility": 0.01, + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440 } ] ``` !!! Tip - This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely difficult to trade with profit. + This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. ### Full example of Pairlist Handlers @@ -160,9 +160,9 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, { - "method": "VolatilityFilter", - "volatility_over_days": 10, - "min_volatility": 0.01, + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440 }, {"method": "ShuffleFilter", "seed": 42} diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 55d802587..2022556d2 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', 'VolatilityFilter'] + 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py similarity index 77% rename from freqtrade/pairlist/volatilityfilter.py rename to freqtrade/pairlist/rangestabilityfilter.py index 14ac0c617..f428bb113 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -15,23 +15,23 @@ from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -class VolatilityFilter(IPairList): +class RangeStabilityFilter(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) - self._days = pairlistconfig.get('volatility_over_days', 10) - self._min_volatility = pairlistconfig.get('min_volatility', 0.01) + self._days = pairlistconfig.get('lookback_days', 10) + self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) if self._days < 1: - raise OperationalException("VolatilityFilter requires volatility_over_days to be >= 1") + raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") if self._days > exchange.ohlcv_candle_limit: - raise OperationalException("VolatilityFilter requires volatility_over_days to not " + raise OperationalException("RangeStabilityFilter requires lookback_days to not " "exceed exchange max request size " f"({exchange.ohlcv_candle_limit})") @@ -48,12 +48,12 @@ class VolatilityFilter(IPairList): """ Short whitelist method description - used for startup-messages """ - return (f"{self.name} - Filtering pairs with volatility below {self._min_volatility} " - f"over the last {plural(self._days, 'day')}.") + 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: """ - Validate volatility + Validate trading range :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, False if it should be removed """ @@ -75,14 +75,14 @@ class VolatilityFilter(IPairList): highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 - if pct_change >= self._min_volatility: + if pct_change >= self._min_rate_of_change: result = True else: self.log_on_refresh(logger.info, f"Removed {pair} from whitelist, " - f"because volatility over {plural(self._days, 'day')} is " + f"because rate of change over {plural(self._days, 'day')} is " f"{pct_change:.3f}, which is below the " - f"threshold of {self._min_volatility}.") + f"threshold of {self._min_rate_of_change}.") result = False self._pair_cache[pair] = result diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index e9df5d3f4..d696e6d02 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -341,8 +341,8 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), ([{"method": "StaticPairList"}, - {"method": "VolatilityFilter", "volatility_over_days": 10, - "min_volatility": 0.01, "refresh_period": 1440}], + {"method": "RangeStabilityFilter", "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, @@ -586,9 +586,9 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count -def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): +def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'VolatilityFilter', 'volatility_over_days': 99999}] + {'method': 'RangeStabilityFilter', 'lookback_days': 99999}] mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -597,27 +597,27 @@ def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): ) with pytest.raises(OperationalException, - match=r'VolatilityFilter requires volatility_over_days to not exceed ' + match=r'RangeStabilityFilter requires lookback_days to not exceed ' r'exchange max request size \([0-9]+\)'): get_patched_freqtradebot(mocker, default_conf) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'VolatilityFilter', 'volatility_over_days': 0}] + {'method': 'RangeStabilityFilter', 'lookback_days': 0}] with pytest.raises(OperationalException, - match='VolatilityFilter requires volatility_over_days to be >= 1'): + match='RangeStabilityFilter requires lookback_days to be >= 1'): get_patched_freqtradebot(mocker, default_conf) -@pytest.mark.parametrize('min_volatility,expected_length', [ +@pytest.mark.parametrize('min_rate_of_change,expected_length', [ (0.01, 5), - (0.05, 0), # Setting volatility to 5% removes all pairs from the whitelist. + (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. ]) -def test_volatilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, - min_volatility, expected_length): +def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, + min_rate_of_change, expected_length): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'VolatilityFilter', 'volatility_over_days': 2, - 'min_volatility': min_volatility}] + {'method': 'RangeStabilityFilter', 'lookback_days': 2, + 'min_rate_of_change': min_rate_of_change}] mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -677,9 +677,9 @@ def test_volatilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_ None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected - ({"method": "VolatilityFilter", "volatility_over_days": 10, "min_volatility": 0.01}, - "[{'VolatilityFilter': 'VolatilityFilter - Filtering pairs with volatility below 0.01 " - "over the last days.'}]", + ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, + "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " + "0.01 over the last days.'}]", None ), ]) From 0d349cb3550bfbbfa6af7915a53789074a76d6a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 19:59:18 +0100 Subject: [PATCH 053/229] Small finetuning --- config_full.json.example | 2 +- freqtrade/exchange/exchange.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 365b6180b..5ee2a1faf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -71,7 +71,7 @@ { "method": "RangeStabilityFilter", "lookback_days": 10, - "min_volatility": 0.01, + "min_rate_of_change": 0.01, "refresh_period": 1440 } ], diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2f52c512f..18f4fbff5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -689,7 +689,7 @@ class Exchange: since_ms: int) -> DataFrame: """ Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe - :param pair: Pair to download + :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from :return: OHLCV DataFrame diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index f428bb113..798d192bd 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -1,5 +1,5 @@ """ -Minimum age (days listed) pair list filter +Rate of change pairlist filter """ import logging from typing import Any, Dict From 8ae604d473df16f769c9cf43d4b37f3eec9f26cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 20:05:06 +0100 Subject: [PATCH 054/229] Ensure we're not running off of empty dataframes --- freqtrade/pairlist/rangestabilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 798d192bd..b460ff477 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -71,7 +71,7 @@ class RangeStabilityFilter(IPairList): timeframe='1d', since_ms=since_ms) result = False - if daily_candles is not None: + if daily_candles is not None and not daily_candles.empty: highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 From 6810192992df1e9f7943728c65c6c02e675c2d92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 20:25:18 +0100 Subject: [PATCH 055/229] Update docstring for new filter --- freqtrade/pairlist/VolumePairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 44e5c52d7..7d3c2c653 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -49,7 +49,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True From c14c0f60a1b8a2fd52c501b5355713b92e3ba100 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 16:27:27 +0100 Subject: [PATCH 056/229] Add Support for kraken stoploss-limit --- docs/exchanges.md | 8 ++++---- docs/stoploss.md | 12 +++++++++--- freqtrade/exchange/kraken.py | 11 ++++++++--- tests/exchange/test_kraken.py | 26 +++++++++++++++----------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 5d7505795..d877e6da2 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,8 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it. + Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. ### Historic Kraken data @@ -75,8 +76,7 @@ print(res) !!! Tip "Stoploss on Exchange" FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. - You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide. - + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used. ### Using subaccounts @@ -99,10 +99,10 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. - ## Random notes for other exchanges * The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed: + ```shell $ pip3 install web3 ``` diff --git a/docs/stoploss.md b/docs/stoploss.md index fa888cd47..7993da401 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -23,11 +23,12 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. - Do not set too low stoploss value if using stop loss on exchange! - If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now. + Do not set too low/tight stoploss value if using stop loss on exchange! + If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio + Enable or Disable stop loss on exchange. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. @@ -40,13 +41,18 @@ Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. +!!! Note + If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. + ### stoploss_on_exchange_interval + In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. ### emergencysell + `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. The below is the default which is used if not changed in strategy or configuration file. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5b7aa5c5b..d66793845 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -77,8 +77,15 @@ class Kraken(Exchange): Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. """ + params = self._params.copy() - ordertype = "stop-loss" + if order_types.get('stoploss', 'market') == 'limit': + ordertype = "stop-loss-limit" + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + limit_rate = stop_price * limit_price_pct + params['price2'] = self.price_to_precision(pair, limit_rate) + else: + ordertype = "stop-loss" stop_price = self.price_to_precision(pair, stop_price) @@ -88,8 +95,6 @@ class Kraken(Exchange): return dry_order try: - params = self._params.copy() - amount = self.amount_to_precision(pair, amount) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 31b79a202..3803658eb 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -10,6 +10,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop-loss' +STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit' def test_buy_kraken_trading_agreement(default_conf, mocker): @@ -156,7 +157,8 @@ def test_get_balances_prod(default_conf, mocker): "get_balances", "fetch_balance") -def test_stoploss_order_kraken(default_conf, mocker): +@pytest.mark.parametrize('ordertype', ['market', 'limit']) +def test_stoploss_order_kraken(default_conf, mocker, ordertype): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -173,24 +175,26 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - # stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) - assert api_mock.create_order.call_count == 1 - - api_mock.create_order.reset_mock() - - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={'stoploss': ordertype, + 'stoploss_on_exchange_limit_ratio': 0.99 + }) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' - assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + if ordertype == 'limit': + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'trading_agreement': 'agree', 'price2': 217.8} + else: + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'trading_agreement': 'agree'} assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 - assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'} # test exception handling with pytest.raises(DependencyException): From d0d9921b42d5e4e10e57d3307d976c1f987c7e6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 16:27:41 +0100 Subject: [PATCH 057/229] Reorder mkdocs sequence --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 8d1ce1cfe..2cc0c9fcb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,13 +20,13 @@ nav: - Hyperopt: hyperopt.md - Edge Positioning: edge.md - Utility Subcommands: utils.md - - Exchange-specific Notes: exchanges.md - FAQ: faq.md - Data Analysis: - Jupyter Notebooks: data-analysis.md - Strategy analysis: strategy_analysis_example.md - Plotting: plotting.md - SQL Cheatsheet: sql_cheatsheet.md + - Exchange-specific Notes: exchanges.md - Advanced Post-installation Tasks: advanced-setup.md - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md From 1d56c87a34850453e88500ee6028ec5e222b3d3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 21:39:12 +0100 Subject: [PATCH 058/229] Fully support kraken limit stoploss --- freqtrade/exchange/exchange.py | 2 +- freqtrade/persistence/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 18f4fbff5..611ce4abd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -524,7 +524,7 @@ class Exchange: 'rate': self.get_fee(pair) } }) - if closed_order["type"] in ["stop_loss_limit"]: + if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: closed_order["info"].update({"stopPrice": closed_order["price"]}) self._dry_run_open_orders[closed_order["id"]] = closed_order diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8160ffbbf..6027908da 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -397,7 +397,7 @@ class Trade(_DECL_BASE): if self.is_open: logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) - elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): + elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss if self.is_open: From 0b68402c1094c89ead68bd4908eec47383005835 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Thu, 26 Nov 2020 10:24:48 +0100 Subject: [PATCH 059/229] Fixed a small typo in the pairlist documentation Signed-off-by: hoeckxer --- 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 149e784bd..5bb02470d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -60,7 +60,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", - "refresh_period": 1800, + "refresh_period": 1800 }], ``` From dddbc799f9b1c8d686a33de95a6f620322a56ec7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Nov 2020 19:40:08 +0100 Subject: [PATCH 060/229] have kraken stoploss-limit support trailing stop --- freqtrade/exchange/kraken.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d66793845..4e4713052 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -69,7 +69,8 @@ class Kraken(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + return (order['type'] in ('stop-loss', 'stop-loss-limit') + and stop_loss > float(order['price'])) @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: From 98118f5e956d02c3f646ccb8feff1992e118cc22 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Thu, 26 Nov 2020 18:46:36 -0600 Subject: [PATCH 061/229] Fix parameter name Correct which parameter name was referred to within the 2nd Note under "Amend last stake amount" --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 56ba13414..2e8f6555f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -177,7 +177,7 @@ In the example above this would mean: This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly. !!! Note - The minimum last stake amount can be configured using `amend_last_stake_amount` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange. + The minimum last stake amount can be configured using `last_stake_amount_min_ratio` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange. #### Static stake amount From fce31447edf2c0466cef8de13e0f8d0a26d71e61 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Thu, 26 Nov 2020 19:38:20 -0600 Subject: [PATCH 062/229] Prevent unintended LaTeX rendering --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 7993da401..14b04c7e0 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -140,7 +140,7 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%) -* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ +* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. From 31449987c08780afc384d524fd2bf9098995ae5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 07:35:12 +0100 Subject: [PATCH 063/229] Fix mkdocs rendering --- docs/stoploss.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 14b04c7e0..1e21fc50d 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -36,8 +36,8 @@ If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the st `stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. -Calculation example: we bought the asset at 100$. -Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. +Calculation example: we bought the asset at 100\$. +Stop-price is 95\$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. @@ -90,6 +90,7 @@ Example of stop loss: ``` For example, simplified math: + * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ @@ -113,7 +114,7 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -10% of 102$ = 91.8$ -* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91.8$. +* now the asset drops in value to 101\$, the stop loss will still be 91.8$ and would trigger at 91.8$. In summary: The stoploss will be adjusted to be always be -10% of the highest observed price. @@ -139,8 +140,8 @@ For example, simplified math: * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%) -* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ +* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increments with -2%) +* now the asset drops in value to 101\$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. @@ -157,7 +158,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai trailing_only_offset_is_reached = True ``` -Configuration (offset is buyprice + 3%): +Configuration (offset is buy-price + 3%): ``` python stoploss = -0.10 @@ -175,7 +176,7 @@ For example, simplified math: * stoploss will remain at 90$ unless asset increases to or above our configured offset * assuming the asset now increases to 103$ (where we have the offset configured) * the stop loss will now be -2% of 103$ = 100.94$ -* now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$ +* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$ !!! Tip Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. From 81d08c4deff25a9bd8b24e6d6847bdc04ccdace0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:16:11 +0100 Subject: [PATCH 064/229] 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 065/229] 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 066/229] 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 067/229] 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 068/229] 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 069/229] 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 070/229] 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 071/229] 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 072/229] 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 073/229] 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 074/229] 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 075/229] 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 076/229] 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 077/229] 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 078/229] 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 079/229] 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 080/229] 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 081/229] 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 082/229] 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 083/229] 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 084/229] 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 085/229] 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 086/229] 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 087/229] 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 088/229] 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 089/229] 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 090/229] 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 091/229] 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 092/229] 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 093/229] 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 094/229] 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 095/229] 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 096/229] 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 097/229] 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 098/229] 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 099/229] 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 100/229] 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 101/229] 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 102/229] 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 103/229] 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 104/229] 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 105/229] 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 106/229] 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 107/229] 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 108/229] 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 109/229] 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 110/229] 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 111/229] 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 112/229] 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 113/229] 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 114/229] 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 115/229] 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 116/229] 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 117/229] 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 118/229] 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 119/229] 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 120/229] 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 121/229] 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 122/229] 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 123/229] 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 124/229] 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 125/229] 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 126/229] 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 127/229] 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 128/229] 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 129/229] 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 130/229] 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 131/229] 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 132/229] 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 133/229] 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 134/229] 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 135/229] 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 136/229] 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 137/229] 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 138/229] 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 139/229] 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 140/229] 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 141/229] 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 142/229] 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 143/229] 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 144/229] 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 145/229] 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 146/229] 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 147/229] 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 148/229] 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 149/229] 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 150/229] 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 151/229] 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 152/229] 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 153/229] 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 154/229] 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 155/229] 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 156/229] 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 157/229] 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 158/229] 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 159/229] 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 160/229] 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 161/229] 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 162/229] 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 163/229] 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 164/229] 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 165/229] 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 166/229] 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 167/229] 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 168/229] 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 169/229] 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 170/229] 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 171/229] 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 172/229] 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 173/229] 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 174/229] 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 175/229] 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 176/229] 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 177/229] 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 178/229] 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 179/229] 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 180/229] 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 181/229] 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 182/229] 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 183/229] 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 184/229] 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 185/229] 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 186/229] 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 187/229] 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 188/229] 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 189/229] 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 190/229] 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 191/229] 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 192/229] 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 193/229] 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 194/229] 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 195/229] 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 196/229] 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 197/229] 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 198/229] 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 199/229] 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 200/229] 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 201/229] 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 202/229] 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 203/229] 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 204/229] 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 205/229] 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 206/229] 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 207/229] 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 208/229] 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 209/229] 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 118a22d0104cf6f8298f0f7c72639c450f0413b0 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 8 Dec 2020 18:04:26 +0100 Subject: [PATCH 210/229] 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 211/229] 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 212/229] 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 213/229] 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 214/229] 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 215/229] 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 216/229] 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 217/229] 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 218/229] 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 219/229] 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 220/229] 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 221/229] 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 222/229] 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 223/229] 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 224/229] 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 225/229] 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 226/229] 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 227/229] 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 228/229] 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 229/229] 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