From 4f5a1e94a7b94c75999404fc06be0d48c7132666 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:30 +0530 Subject: [PATCH 001/460] Add .deepsource.toml Signed-off-by: shubhendra --- .deepsource.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..7a00ca8d6 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,16 @@ +version = 1 + +test_patterns = ["tests/**/test_*.py"] + +exclude_patterns = [ + "docs/**", + "user_data/**", + "build/helpers/**" +] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" From 62d99a0b7406136f24d51e6d704fcc18969a3f5b Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:30 +0530 Subject: [PATCH 002/460] Remove unnecessary comprehension Signed-off-by: shubhendra --- freqtrade/resolvers/strategy_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index b1b66e3ae..19bd014f9 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -196,9 +196,9 @@ class StrategyResolver(IResolver): strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - if any([x == 2 for x in [strategy._populate_fun_len, + if any(x == 2 for x in [strategy._populate_fun_len, strategy._buy_fun_len, - strategy._sell_fun_len]]): + strategy._sell_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy From 537ad059bc3b643eb2d022ddb798ed4d84aa8c78 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:31 +0530 Subject: [PATCH 003/460] Remove unnecessary use of comprehension Signed-off-by: shubhendra --- 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 78f45de0b..465f3d443 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -611,7 +611,7 @@ class LocalTrade(): else: # Not used during backtesting, but might be used by a strategy - sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open] + sel_trades = list(LocalTrade.trades + LocalTrade.trades_open) if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] From 6d6ad035d6cf7fcedac7697fd69c8948f2e3fd76 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:32 +0530 Subject: [PATCH 004/460] Remove length check in favour of truthiness of the object Signed-off-by: shubhendra --- freqtrade/commands/list_commands.py | 2 +- freqtrade/exchange/exchange.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 9e6076dfb..5f53fc824 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -177,7 +177,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: # human-readable formats. print() - if len(pairs): + if pairs: if args.get('print_list', False): # print data as a list, with human-readable summary print(f"{summary_str}: {', '.join(pairs.keys())}.") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5b6e2b20d..9c868df2b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -958,7 +958,7 @@ class Exchange: while True: t = await self._async_fetch_trades(pair, params={self._trades_pagination_arg: from_id}) - if len(t): + if t: # Skip last id since its the key for the next call trades.extend(t[:-1]) if from_id == t[-1][1] or t[-1][0] > until: @@ -990,7 +990,7 @@ class Exchange: # DEFAULT_TRADES_COLUMNS: 1 -> id while True: t = await self._async_fetch_trades(pair, since=since) - if len(t): + if t: since = t[-1][0] trades.extend(t) # Reached the end of the defined-download period From 910e15b17431a8c7be26fde1346d9ef2fb9e45d1 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:33 +0530 Subject: [PATCH 005/460] Remove methods with unnecessary super delegation. Signed-off-by: shubhendra --- freqtrade/plugins/pairlist/PerformanceFilter.py | 5 ----- freqtrade/plugins/protections/cooldown_period.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 7d91bb77c..c1355f655 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -15,11 +15,6 @@ 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: """ diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index f74f83885..197d74c2e 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -15,9 +15,6 @@ class CooldownPeriod(IProtection): has_global_stop: bool = False has_local_stop: bool = True - def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: - super().__init__(config, protection_config) - def _reason(self) -> str: """ LockReason to use From 45da3a70227cc95ee7610e0bb12dabd348b0086e Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:34 +0530 Subject: [PATCH 006/460] Refactor unnecessary `else` / `elif` when `if` block has a `continue` statement Signed-off-by: shubhendra --- freqtrade/configuration/directory_operations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 51310f013..1ce8d1461 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -72,6 +72,5 @@ def copy_sample_files(directory: Path, overwrite: bool = False) -> None: if not overwrite: logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") continue - else: - logger.warning(f"File `{targetfile}` exists already, overwriting.") + logger.warning(f"File `{targetfile}` exists already, overwriting.") shutil.copy(str(sourcedir / source), str(targetfile)) From 4d8183491218783c92bc7230d98d00dffdf9b115 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:34 +0530 Subject: [PATCH 007/460] Merge `isinstance` calls Signed-off-by: shubhendra --- freqtrade/exchange/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index c66db860f..be0a1e483 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -140,7 +140,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT): logger.warning('retrying %s() still for %s times', f.__name__, count) count -= 1 kwargs.update({'count': count}) - if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + if isinstance(ex, (DDosProtection, RetryableOrderError)): # increasing backoff backoff_delay = calculate_backoff(count + 1, retries) logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") From 0a205f52b06ad9b12aa13d03794e9e3dfd780440 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 23 Mar 2021 10:02:32 +0200 Subject: [PATCH 008/460] Optional support for defining hyperopt parameters in a strategy file and reusing common hyperopt/strategy parts. --- freqtrade/optimize/hyperopt.py | 8 ++- freqtrade/optimize/hyperopt_auto.py | 83 +++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/hyper.py | 93 +++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_auto.py create mode 100644 freqtrade/strategy/hyper.py diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..8dd8f01ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -26,6 +26,7 @@ from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -67,8 +68,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.custom_hyperopt.__class__.strategy = self.backtesting.strategy + if self.config['hyperopt'] == 'HyperOptAuto': + self.custom_hyperopt = HyperOptAuto(self.config) + else: + self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py new file mode 100644 index 000000000..788bd5a79 --- /dev/null +++ b/freqtrade/optimize/hyperopt_auto.py @@ -0,0 +1,83 @@ +""" +HyperOptAuto class. +This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement +IHyperStrategy interface. +""" +from typing import Any, Callable, Dict, List +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +# noinspection PyUnresolvedReferences +class HyperOptAuto(IHyperOpt): + """ + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time + Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt + methods can be overridden as well. + """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('buy'): + attr.value = params[attr_name] + return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend + + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('sell'): + attr.value = params[attr_name] + return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend + + def _get_func(self, name) -> Callable: + """ + Return a function defined in Strategy.HyperOpt class, or one defined in super() class. + :param name: function name. + :return: a requested function. + """ + hyperopt_cls = getattr(self.strategy, 'HyperOpt') + default_func = getattr(super(), name) + if hyperopt_cls: + return getattr(hyperopt_cls, name, default_func) + else: + return default_func + + def _generate_indicator_space(self, category): + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + for attr_name, attr in self.strategy.enumerate_parameters(category): + yield attr.get_space(attr_name) + + def _get_indicator_space(self, category, fallback_method_name): + indicator_space = list(self._generate_indicator_space(category)) + if len(indicator_space) > 0: + return indicator_space + else: + return self._get_func(fallback_method_name)() + + def indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('buy', 'indicator_space') + + def sell_indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('sell', 'sell_indicator_space') + + def generate_roi_table(self, params: Dict) -> Dict[int, float]: + return self._get_func('generate_roi_table')(params) + + def roi_space(self) -> List[Dimension]: + return self._get_func('roi_space')() + + def stoploss_space(self) -> List[Dimension]: + return self._get_func('stoploss_space')() + + def generate_trailing_params(self, params: Dict) -> Dict: + return self._get_func('generate_trailing_params')(params) + + def trailing_space(self) -> List[Dimension]: + return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 85148b6ea..80a7c00de 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,4 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.hyper import IHyperStrategy, Parameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py new file mode 100644 index 000000000..41bb836d4 --- /dev/null +++ b/freqtrade/strategy/hyper.py @@ -0,0 +1,93 @@ +""" +IHyperStrategy interface, hyperoptable Parameter class. +This module defines a base class for auto-hyperoptable strategies. +""" +from abc import ABC +from typing import Union, List, Iterator, Tuple + +from skopt.space import Integer, Real, Categorical + +from freqtrade.strategy.interface import IStrategy + + +class Parameter(object): + """ + Defines a parameter that can be optimized by hyperopt. + """ + default: Union[int, float, str, bool] + space: List[Union[int, float, str, bool]] + category: str + + def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, + category: str = None, **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. + :param default: A default value. Required for ints and floats, optional for categorial parameters (first item + from the space will be used). Type of default value determines skopt space used for optimization. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). + """ + assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' + self.value = default + self.space = space + self.category = category + self._space_params = kwargs + if default is None: + assert len(space) > 0 + self.value = space[0] + + def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + """ + Create skopt optimization space. + :param name: A name of parameter field. + :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) + """ + if not self.space: + return None + if isinstance(self.value, int): + assert len(self.space) == 2 + return Integer(*self.space, name=name, **self._space_params) + if isinstance(self.value, float): + assert len(self.space) == 2 + return Real(*self.space, name=name, **self._space_params) + + assert len(self.space) > 0 + return Categorical(self.space, name=name, **self._space_params) + + +class IHyperStrategy(IStrategy, ABC): + """ + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + """ + + def __init__(self, config): + super().__init__(config) + self._load_params(getattr(self, 'buy_params', None)) + self._load_params(getattr(self, 'sell_params', None)) + + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + """ + Find all optimizeable parameters and return (name, attr) iterator. + :param category: + :return: + """ + assert category in ('buy', 'sell', None) + for attr_name in dir(self): + if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. + attr = getattr(self, attr_name) + if isinstance(attr, Parameter): + if category is None or category == attr.category or attr_name.startswith(category + '_'): + yield attr_name, attr + + def _load_params(self, params: dict) -> None: + """ + Set optimizeable parameter values. + :param params: Dictionary with new parameter values. + """ + if not params: + return + for attr_name, attr in self.enumerate_parameters(): + if attr_name in params: + attr.value = params[attr_name] From bb89e44e19825dc9b9c1bca393ced8a646800cf5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 10:32:34 +0200 Subject: [PATCH 009/460] [SQUASH] Address PR comments. * Split Parameter into IntParameter/FloatParameter/CategoricalParameter. * Rename IHyperStrategy to HyperStrategyMixin and use it as mixin. * --hyperopt parameter is now optional if strategy uses HyperStrategyMixin. * Use OperationalException() instead of asserts. --- freqtrade/commands/cli_options.py | 1 + freqtrade/optimize/hyperopt.py | 7 +- freqtrade/optimize/hyperopt_auto.py | 22 ++-- freqtrade/optimize/hyperopt_interface.py | 7 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 149 +++++++++++++++++------ 6 files changed, 138 insertions(+), 51 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..8e9f0c994 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -195,6 +195,7 @@ AVAILABLE_CLI_OPTIONS = { '--hyperopt', help='Specify hyperopt class name which will be used by the bot.', metavar='NAME', + required=False, ), "hyperopt_path": Arg( '--hyperopt-path', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8dd8f01ac..b5ee1da1a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,6 +23,7 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange +from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -68,7 +69,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - if self.config['hyperopt'] == 'HyperOptAuto': + if not self.config.get('hyperopt'): + if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' + 'parameter or add HyperStrategyMixin mixin to your ' + 'strategy class.') self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 788bd5a79..fdc4da14c 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -1,7 +1,7 @@ """ HyperOptAuto class. -This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement -IHyperStrategy interface. +This module implements a convenience auto-hyperopt class, which can be used together with strategies + that implement IHyperStrategy interface. """ from typing import Any, Callable, Dict, List from pandas import DataFrame @@ -13,26 +13,31 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noinspection PyUnresolvedReferences class HyperOptAuto(IHyperOpt): """ - This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time - Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt - methods can be overridden as well. + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. + Most of the time Strategy.HyperOpt class would only implement indicator_space and + sell_indicator_space methods, but other hyperopt methods can be overridden as well. """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend def _get_func(self, name) -> Callable: @@ -49,7 +54,8 @@ class HyperOptAuto(IHyperOpt): return default_func def _generate_indicator_space(self, category): - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') for attr_name, attr in self.strategy.enumerate_parameters(category): yield attr.get_space(attr_name) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..2dd8500a6 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,15 +5,14 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.strategy import IStrategy - +from freqtrade.strategy import IStrategy, HyperStrategyMixin logger = logging.getLogger(__name__) @@ -35,7 +34,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str - strategy: IStrategy + strategy: Union[IStrategy, HyperStrategyMixin] def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 80a7c00de..e395be106 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,5 +2,6 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import IHyperStrategy, Parameter +from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ + CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 41bb836d4..8de986950 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,83 +2,158 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ -from abc import ABC -from typing import Union, List, Iterator, Tuple +from typing import Iterator, Tuple, Any, Optional, Sequence from skopt.space import Integer, Real, Categorical -from freqtrade.strategy.interface import IStrategy +from freqtrade.exceptions import OperationalException -class Parameter(object): +class BaseParameter(object): """ Defines a parameter that can be optimized by hyperopt. """ - default: Union[int, float, str, bool] - space: List[Union[int, float, str, bool]] - category: str + category: Optional[str] + default: Any + value: Any + space: Sequence[Any] - def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, - category: str = None, **kwargs): + def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, + **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. - :param default: A default value. Required for ints and floats, optional for categorial parameters (first item - from the space will be used). Type of default value determines skopt space used for optimization. :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ - assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' - self.value = default - self.space = space + if 'name' in kwargs: + raise OperationalException( + 'Name is determined by parameter field name and can not be specified manually.') self.category = category self._space_params = kwargs - if default is None: - assert len(space) > 0 - self.value = space[0] + self.value = default + self.space = space - def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + def __repr__(self): + return f'{self.__class__.__name__}({self.value})' + + +class IntParameter(BaseParameter): + default: int + value: int + space: Sequence[int] + + def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [min, max]. + :param default: A default value. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Integer. + """ + if len(space) != 2: + raise OperationalException('IntParameter space must be [min, max]') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Integer: """ Create skopt optimization space. :param name: A name of parameter field. - :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) """ - if not self.space: - return None - if isinstance(self.value, int): - assert len(self.space) == 2 - return Integer(*self.space, name=name, **self._space_params) - if isinstance(self.value, float): - assert len(self.space) == 2 - return Real(*self.space, name=name, **self._space_params) + return Integer(*self.space, name=name, **self._space_params) - assert len(self.space) > 0 + +class FloatParameter(BaseParameter): + default: float + value: float + space: Sequence[float] + + def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [min, max]. + :param default: A default value. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Real. + """ + if len(space) != 2: + raise OperationalException('IntParameter space must be [min, max]') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Real: + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ + return Real(*self.space, name=name, **self._space_params) + + +class CategoricalParameter(BaseParameter): + default: Any + value: Any + space: Sequence[Any] + + def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, + category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [a, b, ...]. + :param default: A default value. If not specified, first item from specified space will be + used. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Categorical. + """ + if len(space) < 2: + raise OperationalException( + 'IntParameter space must be [a, b, ...] (at least two parameters)') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Categorical: + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ return Categorical(self.space, name=name, **self._space_params) -class IHyperStrategy(IStrategy, ABC): +class HyperStrategyMixin(object): """ - A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell + strategy logic. """ - def __init__(self, config): - super().__init__(config) + # Hint that class can be used with HyperOptAuto. + HYPER_STRATEGY = 1 + + def __init__(self): + """ + Initialize hyperoptable strategy mixin. + :param config: + """ self._load_params(getattr(self, 'buy_params', None)) self._load_params(getattr(self, 'sell_params', None)) - def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ Find all optimizeable parameters and return (name, attr) iterator. :param category: :return: """ - assert category in ('buy', 'sell', None) + if category not in ('buy', 'sell', None): + raise OperationalException('Category must be one of: "buy", "sell", None.') for attr_name in dir(self): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) - if isinstance(attr, Parameter): - if category is None or category == attr.category or attr_name.startswith(category + '_'): + if issubclass(attr.__class__, BaseParameter): + if category is None or category == attr.category or \ + attr_name.startswith(category + '_'): yield attr_name, attr def _load_params(self, params: dict) -> None: From 2d13e5fd5052511d1077632c281099977a548573 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 11:17:17 +0200 Subject: [PATCH 010/460] [SQUASH] Oopsies. --- freqtrade/optimize/hyperopt_auto.py | 2 +- freqtrade/strategy/hyper.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index fdc4da14c..41f086ad2 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,10 +7,10 @@ from typing import Any, Callable, Dict, List from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt -# noinspection PyUnresolvedReferences class HyperOptAuto(IHyperOpt): """ This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 8de986950..154dcead6 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,7 +2,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ -from typing import Iterator, Tuple, Any, Optional, Sequence +from typing import Iterator, Tuple, Any, Optional, Sequence, Union from skopt.space import Integer, Real, Categorical @@ -22,7 +22,8 @@ class BaseParameter(object): **kwargs): """ Initialize hyperopt-optimizable parameter. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ @@ -37,6 +38,9 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' + def get_space(self, name: str) -> Union[Integer, Real, Categorical]: + raise NotImplementedError() + class IntParameter(BaseParameter): default: int @@ -49,7 +53,8 @@ class IntParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param space: Optimization space, [min, max]. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ @@ -76,7 +81,8 @@ class FloatParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param space: Optimization space, [min, max]. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ From e9f0babe8a7276cdb1fa3f22cb2b21368e209c31 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 16:03:38 +0200 Subject: [PATCH 011/460] [SQUASH] Use HyperStrategyMixin as part of IStrategy interface. --- freqtrade/optimize/hyperopt.py | 4 ---- freqtrade/optimize/hyperopt_auto.py | 10 ---------- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/strategy/__init__.py | 3 +-- freqtrade/strategy/hyper.py | 4 ---- freqtrade/strategy/interface.py | 3 ++- 6 files changed, 6 insertions(+), 24 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b5ee1da1a..4926bf1b3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -70,10 +70,6 @@ class Hyperopt: self.backtesting = Backtesting(self.config) if not self.config.get('hyperopt'): - if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' - 'parameter or add HyperStrategyMixin mixin to your ' - 'strategy class.') self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 41f086ad2..753e8175e 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,7 +7,6 @@ from typing import Any, Callable, Dict, List from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa -from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -19,9 +18,6 @@ class HyperOptAuto(IHyperOpt): """ def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): attr.value = params[attr_name] @@ -30,9 +26,6 @@ class HyperOptAuto(IHyperOpt): return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): attr.value = params[attr_name] @@ -54,9 +47,6 @@ class HyperOptAuto(IHyperOpt): return default_func def _generate_indicator_space(self, category): - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - for attr_name, attr in self.strategy.enumerate_parameters(category): yield attr.get_space(attr_name) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 2dd8500a6..46adf55b8 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,14 +5,14 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.strategy import IStrategy, HyperStrategyMixin +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str - strategy: Union[IStrategy, HyperStrategyMixin] + strategy: IStrategy def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e395be106..a300c601b 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,6 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ - CategoricalParameter +from freqtrade.strategy.hyper import IntParameter, FloatParameter, CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 154dcead6..53a0c6462 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -135,13 +135,9 @@ class HyperStrategyMixin(object): strategy logic. """ - # Hint that class can be used with HyperOptAuto. - HYPER_STRATEGY = 1 - def __init__(self): """ Initialize hyperoptable strategy mixin. - :param config: """ self._load_params(getattr(self, 'buy_params', None)) self._load_params(getattr(self, 'sell_params', None)) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6d40e56cc..b00e0ccb8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -59,7 +60,7 @@ class SellCheckTuple(NamedTuple): sell_type: SellType -class IStrategy(ABC): +class IStrategy(ABC, HyperStrategyMixin): """ Interface for freqtrade strategies Defines the mandatory structure must follow any custom strategies From 11689100e7c2b6a8d8fdcb23060d2f97e3aa206f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 16:24:24 +0200 Subject: [PATCH 012/460] [SQUASH] Fix exception when HyperOpt nested class is not defined. --- freqtrade/optimize/hyperopt_auto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 753e8175e..08269b092 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -39,7 +39,7 @@ class HyperOptAuto(IHyperOpt): :param name: function name. :return: a requested function. """ - hyperopt_cls = getattr(self.strategy, 'HyperOpt') + hyperopt_cls = getattr(self.strategy, 'HyperOpt', None) default_func = getattr(super(), name) if hyperopt_cls: return getattr(hyperopt_cls, name, default_func) From fd45dfd89413cf938f14e9e65335ce8a21f316f1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 25 Mar 2021 10:00:52 +0200 Subject: [PATCH 013/460] [SQUASH] Make skopt imports optional. --- freqtrade/optimize/hyperopt_auto.py | 15 +++++++++------ freqtrade/strategy/hyper.py | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 08269b092..fb8adfe6b 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -3,9 +3,12 @@ HyperOptAuto class. This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement IHyperStrategy interface. """ +from contextlib import suppress from typing import Any, Callable, Dict, List + from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +with suppress(ImportError): + from skopt.space import Dimension from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -57,23 +60,23 @@ class HyperOptAuto(IHyperOpt): else: return self._get_func(fallback_method_name)() - def indicator_space(self) -> List[Dimension]: + def indicator_space(self) -> List['Dimension']: return self._get_indicator_space('buy', 'indicator_space') - def sell_indicator_space(self) -> List[Dimension]: + def sell_indicator_space(self) -> List['Dimension']: return self._get_indicator_space('sell', 'sell_indicator_space') def generate_roi_table(self, params: Dict) -> Dict[int, float]: return self._get_func('generate_roi_table')(params) - def roi_space(self) -> List[Dimension]: + def roi_space(self) -> List['Dimension']: return self._get_func('roi_space')() - def stoploss_space(self) -> List[Dimension]: + def stoploss_space(self) -> List['Dimension']: return self._get_func('stoploss_space')() def generate_trailing_params(self, params: Dict) -> Dict: return self._get_func('generate_trailing_params')(params) - def trailing_space(self) -> List[Dimension]: + def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 53a0c6462..0378be1d5 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,9 +2,11 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ +from contextlib import suppress from typing import Iterator, Tuple, Any, Optional, Sequence, Union -from skopt.space import Integer, Real, Categorical +with suppress(ImportError): + from skopt.space import Integer, Real, Categorical from freqtrade.exceptions import OperationalException @@ -38,7 +40,7 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' - def get_space(self, name: str) -> Union[Integer, Real, Categorical]: + def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: raise NotImplementedError() @@ -62,7 +64,7 @@ class IntParameter(BaseParameter): raise OperationalException('IntParameter space must be [min, max]') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Integer: + def get_space(self, name: str) -> 'Integer': """ Create skopt optimization space. :param name: A name of parameter field. @@ -90,7 +92,7 @@ class FloatParameter(BaseParameter): raise OperationalException('IntParameter space must be [min, max]') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Real: + def get_space(self, name: str) -> 'Real': """ Create skopt optimization space. :param name: A name of parameter field. @@ -121,7 +123,7 @@ class CategoricalParameter(BaseParameter): 'IntParameter space must be [a, b, ...] (at least two parameters)') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Categorical: + def get_space(self, name: str) -> 'Categorical': """ Create skopt optimization space. :param name: A name of parameter field. From 424cd2a91422f0f85d1b4362a38ff7fbb3950798 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 25 Mar 2021 17:58:11 +0200 Subject: [PATCH 014/460] [SQUASH] Use "space" instead of category. --- freqtrade/strategy/hyper.py | 73 +++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0378be1d5..9f7fc3fb6 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -18,13 +18,13 @@ class BaseParameter(object): category: Optional[str] default: Any value: Any - space: Sequence[Any] + opt_range: Sequence[Any] - def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, + def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). @@ -32,10 +32,10 @@ class BaseParameter(object): if 'name' in kwargs: raise OperationalException( 'Name is determined by parameter field name and can not be specified manually.') - self.category = category + self.category = space self._space_params = kwargs self.value = default - self.space = space + self.opt_range = opt_range def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -47,88 +47,97 @@ class BaseParameter(object): class IntParameter(BaseParameter): default: int value: int - space: Sequence[int] + opt_range: Sequence[int] - def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, - **kwargs): + def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, + space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [min, max]. + :param low: lower end of optimization space or [low, high]. + :param high: high end of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if len(space) != 2: - raise OperationalException('IntParameter space must be [min, max]') - super().__init__(space=space, default=default, category=category, **kwargs) + if high is None: + if len(low) != 2: + raise OperationalException('IntParameter space must be [low, high]') + opt_range = low + else: + opt_range = [low, high] + super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Integer': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.space, name=name, **self._space_params) + return Integer(*self.opt_range, name=name, **self._space_params) class FloatParameter(BaseParameter): default: float value: float - space: Sequence[float] + opt_range: Sequence[float] - def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, - **kwargs): + def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, + default: float, space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [min, max]. + :param low: lower end of optimization space or [low, high]. + :param high: high end of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if len(space) != 2: - raise OperationalException('IntParameter space must be [min, max]') - super().__init__(space=space, default=default, category=category, **kwargs) + if high is None: + if len(low) != 2: + raise OperationalException('IntParameter space must be [low, high]') + opt_range = low + else: + opt_range = [low, high] + super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Real': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.space, name=name, **self._space_params) + return Real(*self.opt_range, name=name, **self._space_params) class CategoricalParameter(BaseParameter): default: Any value: Any - space: Sequence[Any] + opt_range: Sequence[Any] - def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, - category: Optional[str] = None, - **kwargs): + def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, + space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [a, b, ...]. + :param categories: Optimization space, [a, b, ...]. :param default: A default value. If not specified, first item from specified space will be used. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Categorical. """ - if len(space) < 2: + if len(categories) < 2: raise OperationalException( 'IntParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(space=space, default=default, category=category, **kwargs) + super().__init__(opt_range=categories, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Categorical': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Categorical(self.space, name=name, **self._space_params) + return Categorical(self.opt_range, name=name, **self._space_params) class HyperStrategyMixin(object): From bbe6ece38ddd89ca3c4174e7c9a5d824216a9a98 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 14:25:17 +0200 Subject: [PATCH 015/460] [SQUASH] Fix parameter configs not loading. --- freqtrade/strategy/hyper.py | 2 +- freqtrade/strategy/interface.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 9f7fc3fb6..0b3021af2 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -146,7 +146,7 @@ class HyperStrategyMixin(object): strategy logic. """ - def __init__(self): + def __init__(self, *args, **kwargs): """ Initialize hyperoptable strategy mixin. """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b00e0ccb8..54c7f2353 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -141,6 +141,7 @@ class IStrategy(ABC, HyperStrategyMixin): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} + super().__init__(config) @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From 40f5c7853ed06422c53e3e320f670266f0f6cb64 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 14:25:49 +0200 Subject: [PATCH 016/460] [SQUASH] Add a way to temporarily disable a parameter (excludes from parameter loading/hyperopt) and print parameter values when executed. --- freqtrade/optimize/hyperopt_auto.py | 3 ++- freqtrade/strategy/hyper.py | 32 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index fb8adfe6b..31a11e303 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -51,7 +51,8 @@ class HyperOptAuto(IHyperOpt): def _generate_indicator_space(self, category): for attr_name, attr in self.strategy.enumerate_parameters(category): - yield attr.get_space(attr_name) + if attr.enabled: + yield attr.get_space(attr_name) def _get_indicator_space(self, category, fallback_method_name): indicator_space = list(self._generate_indicator_space(category)) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0b3021af2..e6f63f5a5 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,6 +2,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ +import logging from contextlib import suppress from typing import Iterator, Tuple, Any, Optional, Sequence, Union @@ -11,6 +12,9 @@ with suppress(ImportError): from freqtrade.exceptions import OperationalException +logger = logging.getLogger(__name__) + + class BaseParameter(object): """ Defines a parameter that can be optimized by hyperopt. @@ -21,7 +25,7 @@ class BaseParameter(object): opt_range: Sequence[Any] def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, - **kwargs): + enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if @@ -36,6 +40,7 @@ class BaseParameter(object): self._space_params = kwargs self.value = default self.opt_range = opt_range + self.enabled = enabled def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -50,7 +55,7 @@ class IntParameter(BaseParameter): opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, - space: Optional[str] = None, **kwargs): + space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: lower end of optimization space or [low, high]. @@ -67,7 +72,8 @@ class IntParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Integer': """ @@ -83,7 +89,7 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, - default: float, space: Optional[str] = None, **kwargs): + default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: lower end of optimization space or [low, high]. @@ -100,7 +106,8 @@ class FloatParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Real': """ @@ -116,7 +123,7 @@ class CategoricalParameter(BaseParameter): opt_range: Sequence[Any] def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, - space: Optional[str] = None, **kwargs): + space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. @@ -130,7 +137,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'IntParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, **kwargs) + super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Categorical': """ @@ -167,7 +175,8 @@ class HyperStrategyMixin(object): if issubclass(attr.__class__, BaseParameter): if category is None or category == attr.category or \ attr_name.startswith(category + '_'): - yield attr_name, attr + if attr.enabled: + yield attr_name, attr def _load_params(self, params: dict) -> None: """ @@ -178,4 +187,9 @@ class HyperStrategyMixin(object): return for attr_name, attr in self.enumerate_parameters(): if attr_name in params: - attr.value = params[attr_name] + if attr.enabled: + attr.value = params[attr_name] + logger.info(f'attr_name = {attr.value}') + else: + logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' + f'Default value "{attr.value}" used.') From e934d3ddfbd32ab5cc897883d96c242eec66c5db Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 16:55:48 +0200 Subject: [PATCH 017/460] [SQUASH] Oopsie. --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e6f63f5a5..51f937fca 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -88,7 +88,7 @@ class FloatParameter(BaseParameter): value: float opt_range: Sequence[float] - def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, + def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. From 786ddc6a9114dd8b26978be7bbeb1c67db44f48a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 10:40:48 +0100 Subject: [PATCH 018/460] remove unused imports --- freqtrade/optimize/hyperopt.py | 1 - freqtrade/optimize/hyperopt_auto.py | 2 ++ freqtrade/optimize/hyperopt_interface.py | 1 + freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/hyper.py | 19 ++++++++++--------- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4926bf1b3..a680d85c8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,6 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 31a11e303..847d8697a 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,6 +7,8 @@ from contextlib import suppress from typing import Any, Callable, Dict, List from pandas import DataFrame + + with suppress(ImportError): from skopt.space import Dimension diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 46adf55b8..561fb8e11 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -14,6 +14,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict from freqtrade.strategy import IStrategy + logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index a300c601b..bc0c45f7c 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) +from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import IntParameter, FloatParameter, CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 51f937fca..32b03d57e 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -4,7 +4,8 @@ This module defines a base class for auto-hyperoptable strategies. """ import logging from contextlib import suppress -from typing import Iterator, Tuple, Any, Optional, Sequence, Union +from typing import Any, Iterator, Optional, Sequence, Tuple, Union + with suppress(ImportError): from skopt.space import Integer, Real, Categorical @@ -58,12 +59,12 @@ class IntParameter(BaseParameter): space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param low: lower end of optimization space or [low, high]. - :param high: high end of optimization space. Must be none of entire range is passed first parameter. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ if high is None: @@ -92,12 +93,12 @@ class FloatParameter(BaseParameter): default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param low: lower end of optimization space or [low, high]. - :param high: high end of optimization space. Must be none of entire range is passed first parameter. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ if high is None: From 71e2134694c88815d0c4b39e704abdbfbcadfa30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:26:26 +0100 Subject: [PATCH 019/460] Add some simple tests for hyperoptParameters --- freqtrade/strategy/hyper.py | 14 +- tests/rpc/test_rpc_apiserver.py | 6 +- .../strategy/strats/hyperoptable_strategy.py | 170 ++++++++++++++++++ tests/strategy/test_interface.py | 38 +++- tests/strategy/test_strategy_loading.py | 6 +- 5 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/hyperoptable_strategy.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32b03d57e..b8bfef767 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -67,8 +67,10 @@ class IntParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is None: - if len(low) != 2: + if high is not None and isinstance(low, Sequence): + raise OperationalException('IntParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') opt_range = low else: @@ -101,9 +103,11 @@ class FloatParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is None: - if len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') + if high is not None and isinstance(low, Sequence): + raise OperationalException('FloatParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException('FloatParameter space must be [low, high]') opt_range = low else: opt_range = [low, high] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..bef70a5dd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1149,7 +1149,11 @@ def test_api_strategies(botclient): rc = client_get(client, f"{BASE_URI}/strategies") assert_response(rc) - assert rc.json() == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + assert rc.json() == {'strategies': [ + 'DefaultStrategy', + 'HyperoptableStrategy', + 'TestStrategyLegacy' + ]} def test_api_strategy(botclient): diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py new file mode 100644 index 000000000..8cde28321 --- /dev/null +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -0,0 +1,170 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy import FloatParameter, IntParameter, IStrategy + + +class HyperoptableStrategy(IStrategy): + """ + Default Strategy provided by freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal ticker interval for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74 + } + + buy_rsi = IntParameter([0, 50], default=30, space='buy') + buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['rsi'] < self.buy_rsi.value) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), + 'sell'] = 1 + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f158a1518..9c831d194 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -10,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -552,3 +553,38 @@ def test_strategy_safe_wrapper(value): assert type(ret) == type(value) assert ret == value + + +def test_hyperopt_parameters(): + with pytest.raises(OperationalException, match=r"Name is determined.*"): + IntParameter(low=0, high=5, default=1, name='hello') + + with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): + IntParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): + FloatParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): + IntParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): + FloatParameter([0, 10], high=7, default=5, space='buy') + + x = BaseParameter(opt_range=[0, 1], default=1, space='buy') + with pytest.raises(NotImplementedError): + x.get_space('space') + + fltpar = IntParameter(low=0, high=5, default=1, space='buy') + assert fltpar.value == 1 + + +def test_auto_hyperopt_interface(default_conf): + default_conf.update({'strategy': 'HyperoptableStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi'] + # PlusDI is NOT in the buy-params, so default should be used + assert strategy.buy_plusdi.value == 0.5 + assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 1c692d2da..965c3d37b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 2 + assert len(strategies) == 3 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 2 + assert len([x for x in strategies if x['class'] is not None]) == 3 assert len([x for x in strategies if x['class'] is None]) == 1 From 4fd7bedcb28f98a8920e51b41a5b7300e62f85de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:32:51 +0100 Subject: [PATCH 020/460] Sort imports ... --- tests/strategy/test_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 9c831d194..5dd459238 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -14,6 +13,7 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re From 8022386404e4aca3fdad77e8c43464990eb11c0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 18:00:07 +0100 Subject: [PATCH 021/460] Type custom_hyperopt --- freqtrade/optimize/hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a680d85c8..f7c2da4ec 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -62,6 +62,7 @@ class Hyperopt: hyperopt = Hyperopt(config) hyperopt.start() """ + custom_hyperopt: IHyperOpt def __init__(self, config: Dict[str, Any]) -> None: self.config = config From 20f7e9b4b79dcc9db09592826289d7a030ec5558 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 19:31:54 +0200 Subject: [PATCH 022/460] Make BaseParameter get_space abstract --- freqtrade/strategy/hyper.py | 8 ++++++-- tests/strategy/test_interface.py | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index b8bfef767..461e6314f 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -3,6 +3,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ import logging +from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Iterator, Optional, Sequence, Tuple, Union @@ -16,7 +17,7 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -class BaseParameter(object): +class BaseParameter(ABC): """ Defines a parameter that can be optimized by hyperopt. """ @@ -46,8 +47,11 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' + @abstractmethod def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: - raise NotImplementedError() + """ + Get-space - will be used by Hyperopt to get the hyperopt Space + """ class IntParameter(BaseParameter): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5dd459238..e9d57dd17 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -571,9 +571,8 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): FloatParameter([0, 10], high=7, default=5, space='buy') - x = BaseParameter(opt_range=[0, 1], default=1, space='buy') - with pytest.raises(NotImplementedError): - x.get_space('space') + with pytest.raises(TypeError): + BaseParameter(opt_range=[0, 1], default=1, space='buy') fltpar = IntParameter(low=0, high=5, default=1, space='buy') assert fltpar.value == 1 From 929f3296079b4536bfb163b544dcf4ffda46f6ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 19:49:20 +0200 Subject: [PATCH 023/460] more tests --- freqtrade/strategy/hyper.py | 2 +- .../strategy/strats/hyperoptable_strategy.py | 6 ++++-- tests/strategy/test_interface.py | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 461e6314f..64e457d75 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -145,7 +145,7 @@ class CategoricalParameter(BaseParameter): """ if len(categories) < 2: raise OperationalException( - 'IntParameter space must be [a, b, ...] (at least two parameters)') + 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, **kwargs) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 8cde28321..97d2092d4 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -55,12 +55,14 @@ class HyperoptableStrategy(IStrategy): } sell_params = { - 'sell_rsi': 74 + 'sell_rsi': 74, + 'sell_minusdi': 0.4 } buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', enabled=False) def informative_pairs(self): """ @@ -164,7 +166,7 @@ class HyperoptableStrategy(IStrategy): ) | ( (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) + (dataframe['minus_di'] > self.sell_minusdi.value) ), 'sell'] = 1 return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e9d57dd17..72b78338d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,7 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, + IntParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -556,6 +557,7 @@ def test_strategy_safe_wrapper(value): def test_hyperopt_parameters(): + from skopt.space import Categorical, Integer, Real with pytest.raises(OperationalException, match=r"Name is determined.*"): IntParameter(low=0, high=5, default=1, name='hello') @@ -571,12 +573,24 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): + CategoricalParameter(['aa'], default='aa', space='buy') + with pytest.raises(TypeError): BaseParameter(opt_range=[0, 1], default=1, space='buy') - fltpar = IntParameter(low=0, high=5, default=1, space='buy') + intpar = IntParameter(low=0, high=5, default=1, space='buy') + assert intpar.value == 1 + assert isinstance(intpar.get_space(''), Integer) + + fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') + assert isinstance(catpar.get_space(''), Categorical) + assert catpar.value == 'buy_macd' + def test_auto_hyperopt_interface(default_conf): default_conf.update({'strategy': 'HyperoptableStrategy'}) @@ -587,3 +601,6 @@ def test_auto_hyperopt_interface(default_conf): # PlusDI is NOT in the buy-params, so default should be used assert strategy.buy_plusdi.value == 0.5 assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] + + # Parameter is disabled - so value from sell_param dict will NOT be used. + assert strategy.sell_minusdi.value == 0.5 From f6211bc00ebe43b4f8ae981658dc9f11a6c711aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 20:19:39 +0200 Subject: [PATCH 024/460] new-config should include API config --- freqtrade/commands/build_config_commands.py | 30 +++++++++++++++++++++ freqtrade/templates/base_config.json.j2 | 10 +++---- tests/commands/test_build_config.py | 4 +++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 3c34ff162..0dee480b3 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -1,4 +1,5 @@ import logging +import secrets from pathlib import Path from typing import Any, Dict, List @@ -138,6 +139,32 @@ def ask_user_config() -> Dict[str, Any]: "message": "Insert Telegram chat id", "when": lambda x: x['telegram'] }, + { + "type": "confirm", + "name": "api_server", + "message": "Do you want to enable the Rest API (includes FreqUI)?", + "default": False, + }, + { + "type": "text", + "name": "api_server_listen_addr", + "message": "Insert Api server Listen Address (best left untouched default!)", + "default": "127.0.0.1", + "when": lambda x: x['api_server'] + }, + { + "type": "text", + "name": "api_server_username", + "message": "Insert api-server username", + "default": "freqtrader", + "when": lambda x: x['api_server'] + }, + { + "type": "text", + "name": "api_server_password", + "message": "Insert api-server password", + "when": lambda x: x['api_server'] + }, ] answers = prompt(questions) @@ -145,6 +172,9 @@ def ask_user_config() -> Dict[str, Any]: # Interrupted questionary sessions return an empty dict. raise OperationalException("User interrupted interactive questions.") + # Force JWT token to be a random string + answers['api_server_jwt_key'] = secrets.token_hex() + return answers diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 226bf1a81..42f088f9f 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -54,15 +54,15 @@ "chat_id": "{{ telegram_chat_id }}" }, "api_server": { - "enabled": false, - "listen_ip_address": "127.0.0.1", + "enabled": {{ api_server | lower }}, + "listen_ip_address": "{{ api_server_listen_addr | default("127.0.0.1", true) }}", "listen_port": 8080, "verbosity": "error", "enable_openapi": false, - "jwt_secret_key": "somethingrandom", + "jwt_secret_key": "{{ api_server_jwt_key }}", "CORS_origins": [], - "username": "", - "password": "" + "username": "{{ api_server_username }}", + "password": "{{ api_server_password }}" }, "bot_name": "freqtrade", "initial_state": "running", diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py index 291720f4b..66c750e79 100644 --- a/tests/commands/test_build_config.py +++ b/tests/commands/test_build_config.py @@ -50,6 +50,10 @@ def test_start_new_config(mocker, caplog, exchange): 'telegram': False, 'telegram_token': 'asdf1244', 'telegram_chat_id': '1144444', + 'api_server': False, + 'api_server_listen_addr': '127.0.0.1', + 'api_server_username': 'freqtrader', + 'api_server_password': 'MoneyMachine', } mocker.patch('freqtrade.commands.build_config_commands.ask_user_config', return_value=sample_selections) From 932284574077efca1c6cb1b578e2df48dc93540d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:27:22 +0000 Subject: [PATCH 025/460] Bump mkdocs-material from 7.0.6 to 7.0.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.6 to 7.0.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/7.0.6...7.0.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 0068dd5d2..711b6ca46 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.6 +mkdocs-material==7.0.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From e5789b36cf8d6488abf1a8e0e2b71585c1893e9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:27:57 +0000 Subject: [PATCH 026/460] Bump numpy from 1.20.1 to 1.20.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.20.1 to 1.20.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.20.1...v1.20.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..201ce22fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.20.1 +numpy==1.20.2 pandas==1.2.3 ccxt==1.43.89 From 607c05b3cec2523eec63b87639c64adec8bf928f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:28:16 +0000 Subject: [PATCH 027/460] Bump sqlalchemy from 1.4.2 to 1.4.3 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..ff5412fb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.2 +SQLAlchemy==1.4.3 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 8e49271e6f948a37695438e8e57e43053e0c623a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:28:30 +0000 Subject: [PATCH 028/460] Bump prompt-toolkit from 3.0.17 to 3.0.18 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.17 to 3.0.18. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.17...3.0.18) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..127b9a776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,4 +39,4 @@ aiofiles==0.6.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.17 +prompt-toolkit==3.0.18 From 95a9c92769c389fd44693ee8c6fdfa92755e43d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 09:13:48 +0200 Subject: [PATCH 029/460] Add permission-check before slack notify --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61ecaa522..dd6af0a06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,9 +310,17 @@ jobs: needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: + - name: Check user permission + id: check + uses: scherermichael-oss/action-has-permission@1.0.6 + with: + required-permission: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Slack Notification uses: lazy-actions/slatify@v3.0.0 - if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade CI*' From 8d01767a421113c80870b392175effbfd3300def Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 09:20:34 +0200 Subject: [PATCH 030/460] Fix CI syntax --- .github/workflows/ci.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd6af0a06..8e15a5a89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,13 +310,14 @@ jobs: needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: + - name: Check user permission - id: check - uses: scherermichael-oss/action-has-permission@1.0.6 - with: - required-permission: write - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + id: check + uses: scherermichael-oss/action-has-permission@1.0.6 + with: + required-permission: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Slack Notification uses: lazy-actions/slatify@v3.0.0 From dacaa4a732a396315eeb21f58301d7cccbef7056 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:34:57 +0000 Subject: [PATCH 031/460] Bump ccxt from 1.43.89 to 1.45.44 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.43.89 to 1.45.44. - [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.43.89...1.45.44) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10225f910..d3b8acb25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.43.89 +ccxt==1.45.44 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 3e864a87ad66b10058df601a4f7759dfe236ff70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:53:50 +0000 Subject: [PATCH 032/460] Bump cryptography from 3.4.6 to 3.4.7 Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.6 to 3.4.7. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.6...3.4.7) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10225f910..a57233823 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.3 ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels -cryptography==3.4.6 +cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.3 python-telegram-bot==13.4.1 From 5d5debab667facfb0bed113ed878392d4b52caeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 10:01:20 +0000 Subject: [PATCH 033/460] Bump scipy from 1.6.1 to 1.6.2 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.1 to 1.6.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.1...v1.6.2) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 8cdb6fd28..9eb490f83 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.1 +scipy==1.6.2 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 From 6954a1e02951e587cee1d6b8e46365913ace34cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 19:27:19 +0200 Subject: [PATCH 034/460] MOre tests for ParameterHyperopt --- tests/optimize/conftest.py | 2 ++ tests/optimize/test_hyperopt.py | 17 +++++++++++++++++ tests/strategy/test_interface.py | 3 ++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index df6f22e01..5c789ec1e 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -14,6 +14,7 @@ from tests.conftest import patch_exchange def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ + 'datadir': Path(default_conf['datadir']), 'hyperopt': 'DefaultHyperOpt', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), @@ -21,6 +22,7 @@ def hyperopt_conf(default_conf): 'timerange': None, 'spaces': ['default'], 'hyperopt_jobs': 1, + 'hyperopt_min_trades': 1, }) return hyperconf diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 193d997db..36b6f1229 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,6 +16,7 @@ from freqtrade.commands.optimize_commands import setup_optimize_configuration, s from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -1089,3 +1090,19 @@ def test_print_epoch_details(capsys): assert '# ROI table:' in captured.out assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE) assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) + + +def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: + # mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + # mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # No hyperopt needed + del hyperopt_conf['hyperopt'] + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + }) + hyperopt = Hyperopt(hyperopt_conf) + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + hyperopt.start() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 72b78338d..4d93f7049 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -587,7 +587,8 @@ def test_hyperopt_parameters(): assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 - catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], + default='buy_macd', space='buy') assert isinstance(catpar.get_space(''), Categorical) assert catpar.value == 'buy_macd' From 89bbfd2324ebf0974c60f1b8689941a079b14eea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 20:22:52 +0200 Subject: [PATCH 035/460] Remove candle_count from dataframe before backtesting closes #3754 --- freqtrade/data/converter.py | 16 +++++++++++----- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/optimize/hyperopt.py | 3 ++- tests/data/test_converter.py | 10 ++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index d4053abaa..defc96ef8 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -115,17 +115,23 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) return df -def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date') -> DataFrame: +def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', + startup_candles: int = 0) -> DataFrame: """ Trim dataframe based on given timerange :param df: Dataframe to trim :param timerange: timerange (use start and end date if available) - :param: df_date_col: Column in the dataframe to use as Date column + :param df_date_col: Column in the dataframe to use as Date column + :param startup_candles: When not 0, is used instead the timerange start date :return: trimmed dataframe """ - if timerange.starttype == 'date': - start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) - df = df.loc[df[df_date_col] >= start, :] + if startup_candles: + # Trim candles instead of timeframe in case of given startup_candle count + df = df.iloc[startup_candles:, :] + else: + if timerange.starttype == 'date': + start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) + df = df.loc[df[df_date_col] >= start, :] if timerange.stoptype == 'date': stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) df = df.loc[df[df_date_col] <= stop, :] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 765e2844a..ff1dd934c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -443,7 +443,8 @@ class Backtesting: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) + preprocessed[pair] = trim_dataframe(df, timerange, + startup_candles=self.required_startup) min_date, max_date = history.get_timerange(preprocessed) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..ee453489d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,8 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) + preprocessed[pair] = trim_dataframe(df, timerange, + startup_candles=self.backtesting.required_startup) min_date, max_date = get_timerange(preprocessed) logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 4fdcce4d2..2420dca8f 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -197,6 +197,16 @@ def test_trim_dataframe(testdatadir) -> None: assert all(data_modify.iloc[-1] == data.iloc[-1]) assert all(data_modify.iloc[0] == data.iloc[30]) + data_modify = data.copy() + tr = TimeRange('date', None, min_date + 1800, 0) + # Remove first 20 candles - ignores min date + data_modify = trim_dataframe(data_modify, tr, startup_candles=20) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 20 + assert all(data_modify.iloc[-1] == data.iloc[-1]) + assert all(data_modify.iloc[0] == data.iloc[20]) + data_modify = data.copy() # Remove last 30 minutes (1800 s) tr = TimeRange(None, 'date', 0, max_date - 1800) From 50fcb3f330a16a477e3fdf48243f1d3accad6402 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Mar 2021 07:26:39 +0200 Subject: [PATCH 036/460] Reduce verbosity of missing data if less than 1% of data is missing --- freqtrade/data/converter.py | 9 ++++++++- tests/data/test_converter.py | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index defc96ef8..c9d4ef19f 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -110,8 +110,15 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) df.reset_index(inplace=True) len_before = len(dataframe) len_after = len(df) + pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 if len_before != len_after: - logger.info(f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}") + message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" + f" - {round(pct_missing * 100, 2)} %") + if pct_missing > 0.01: + logger.info(message) + else: + # Don't be verbose if only a small amount is missing + logger.debug(message) return df diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 2420dca8f..68960af1c 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -10,7 +10,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma 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 +from tests.conftest import log_has, log_has_re from tests.data.test_history import _backup_file, _clean_test_file @@ -62,8 +62,8 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", caplog) + assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}.*", caplog) # Test fillup actually fixes invalid backtest data min_date, max_date = get_timerange({'UNITTEST/BTC': data}) @@ -125,8 +125,8 @@ def test_ohlcv_fill_up_missing_data2(caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", caplog) + assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}.*", caplog) def test_ohlcv_drop_incomplete(caplog): From 2869d5368de5868b7dfb0c72d402449cf72c2997 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Mar 2021 20:20:24 +0200 Subject: [PATCH 037/460] Allow edge to use dynamic pairlists closes #4298 --- docs/edge.md | 6 +++--- freqtrade/configuration/config_validation.py | 5 ----- freqtrade/edge/edge_positioning.py | 5 ++--- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/edge_cli.py | 2 +- tests/edge/test_edge.py | 8 ++++---- tests/test_configuration.py | 16 ---------------- 7 files changed, 11 insertions(+), 33 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 5565ca2f9..0aa76cd12 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,9 +1,9 @@ # Edge positioning -The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. +The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. !!! Warning - `Edge positioning` is not compatible with dynamic (volume-based) whitelist. + WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data. !!! Note `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. @@ -14,7 +14,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. +To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. !!! tip "It doesn't matter how often, but how much!" A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index c7e49f33d..31e38d572 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -149,11 +149,6 @@ def _validate_edge(conf: Dict[str, Any]) -> None: if not conf.get('edge', {}).get('enabled'): return - if conf.get('pairlist', {}).get('method') == 'VolumePairList': - raise OperationalException( - "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." diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index ff86e522e..d1f76c21f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -84,9 +84,8 @@ class Edge: self.fee = self.exchange.get_fee(symbol=expand_pairlist( self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) - def calculate(self) -> bool: - pairs = expand_pairlist(self.config['exchange']['pair_whitelist'], - list(self.exchange.markets)) + def calculate(self, pairs: List[str]) -> bool: + heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73f4c91be..dd6966848 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -225,7 +225,7 @@ class FreqtradeBot(LoggingMixin): # Calculating Edge positioning if self.edge: - self.edge.calculate() + self.edge.calculate(_whitelist) _whitelist = self.edge.adjust(_whitelist) if trades: diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a5f505bee..aab7def05 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -44,7 +44,7 @@ class EdgeCli: 'timerange') is None else str(self.config.get('timerange'))) def start(self) -> None: - result = self.edge.calculate() + result = self.edge.calculate(self.config['exchange']['pair_whitelist']) if result: print('') # blank line for readability print(generate_edge_table(self.edge._cached_pairs)) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index c30bce6a4..5142dd985 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -266,7 +266,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): # should not recalculate if heartbeat not reached edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1 - assert edge.calculate() is False + assert edge.calculate(edge_conf['exchange']['pair_whitelist']) is False def mocked_load_data(datadir, pairs=[], timeframe='0m', @@ -310,7 +310,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert edge.calculate() + assert edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 2 assert edge._last_updated <= arrow.utcnow().int_timestamp + 2 @@ -322,7 +322,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert not edge.calculate() + assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 0 assert log_has("No data found. Edge is stopped ...", caplog) assert edge._last_updated == 0 @@ -337,7 +337,7 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert not edge.calculate() + assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 0 assert log_has("No trades found.", caplog) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a0824e65c..15fbab7f8 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -860,22 +860,6 @@ def test_validate_tsl(default_conf): validate_config_consistency(default_conf) -def test_validate_edge(edge_conf): - edge_conf.update({"pairlist": { - "method": "VolumePairList", - }}) - - with pytest.raises(OperationalException, - match="Edge and VolumePairList are incompatible, " - "Edge will override whatever pairs VolumePairlist selects."): - validate_config_consistency(edge_conf) - - edge_conf.update({"pairlist": { - "method": "StaticPairList", - }}) - validate_config_consistency(edge_conf) - - def test_validate_edge2(edge_conf): edge_conf.update({"ask_strategy": { "use_sell_signal": True, From 5e5b11d4d60c6f7f8544e8f1cd8064df01f18a6d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 31 Mar 2021 12:31:28 +0300 Subject: [PATCH 038/460] Split "enabled" to "load" and "optimize" parameters. --- freqtrade/optimize/hyperopt_auto.py | 8 ++++--- freqtrade/strategy/hyper.py | 36 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 847d8697a..0e3cf7eac 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -25,7 +25,8 @@ class HyperOptAuto(IHyperOpt): def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): - attr.value = params[attr_name] + if attr.optimize: + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -33,7 +34,8 @@ class HyperOptAuto(IHyperOpt): def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): - attr.value = params[attr_name] + if attr.optimize: + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend @@ -53,7 +55,7 @@ class HyperOptAuto(IHyperOpt): def _generate_indicator_space(self, category): for attr_name, attr in self.strategy.enumerate_parameters(category): - if attr.enabled: + if attr.optimize: yield attr.get_space(attr_name) def _get_indicator_space(self, category, fallback_method_name): diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 64e457d75..c06ae36da 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -27,12 +27,14 @@ class BaseParameter(ABC): opt_range: Sequence[Any] def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, - enabled: bool = True, **kwargs): + optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ if 'name' in kwargs: @@ -42,7 +44,8 @@ class BaseParameter(ABC): self._space_params = kwargs self.value = default self.opt_range = opt_range - self.enabled = enabled + self.optimize = optimize + self.load = load def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -60,7 +63,7 @@ class IntParameter(BaseParameter): opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, - space: Optional[str] = None, enabled: bool = True, **kwargs): + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. @@ -69,6 +72,8 @@ class IntParameter(BaseParameter): :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ if high is not None and isinstance(low, Sequence): @@ -79,8 +84,8 @@ class IntParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Integer': """ @@ -96,7 +101,7 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. @@ -105,6 +110,8 @@ class FloatParameter(BaseParameter): :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): @@ -115,8 +122,8 @@ class FloatParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Real': """ @@ -132,7 +139,7 @@ class CategoricalParameter(BaseParameter): opt_range: Sequence[Any] def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, - space: Optional[str] = None, enabled: bool = True, **kwargs): + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. @@ -141,13 +148,15 @@ class CategoricalParameter(BaseParameter): :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Categorical. """ if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': """ @@ -184,8 +193,7 @@ class HyperStrategyMixin(object): if issubclass(attr.__class__, BaseParameter): if category is None or category == attr.category or \ attr_name.startswith(category + '_'): - if attr.enabled: - yield attr_name, attr + yield attr_name, attr def _load_params(self, params: dict) -> None: """ @@ -196,7 +204,7 @@ class HyperStrategyMixin(object): return for attr_name, attr in self.enumerate_parameters(): if attr_name in params: - if attr.enabled: + if attr.load: attr.value = params[attr_name] logger.info(f'attr_name = {attr.value}') else: From 5acdc9bf424e6c46329a5f886182b5317321846a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 06:47:23 +0200 Subject: [PATCH 039/460] Fix type errors by converting all hyperopt methods to instance methods --- freqtrade/optimize/hyperopt_interface.py | 33 +++++++++--------------- freqtrade/strategy/hyper.py | 3 ++- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..e951efddd 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -44,36 +44,31 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: """ Create a buy strategy generator. """ raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy')) - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: """ Create a sell strategy generator. """ raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell')) - @staticmethod - def indicator_space() -> List[Dimension]: + def indicator_space(self) -> List[Dimension]: """ Create an indicator space. """ raise OperationalException(_format_exception_message('indicator_space', 'buy')) - @staticmethod - def sell_indicator_space() -> List[Dimension]: + def sell_indicator_space(self) -> List[Dimension]: """ Create a sell indicator space. """ raise OperationalException(_format_exception_message('sell_indicator_space', 'sell')) - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: + def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. @@ -88,8 +83,7 @@ class IHyperOpt(ABC): return roi_table - @staticmethod - def roi_space() -> List[Dimension]: + def roi_space(self) -> List[Dimension]: """ Create a ROI space. @@ -109,7 +103,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_min = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_min = timeframe_to_minutes(self.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: @@ -145,7 +139,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -154,7 +148,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), @@ -165,8 +159,7 @@ class IHyperOpt(ABC): Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), ] - @staticmethod - def stoploss_space() -> List[Dimension]: + def stoploss_space(self) -> List[Dimension]: """ Create a stoploss space. @@ -177,8 +170,7 @@ class IHyperOpt(ABC): Real(-0.35, -0.02, name='stoploss'), ] - @staticmethod - def generate_trailing_params(params: Dict) -> Dict: + def generate_trailing_params(self, params: Dict) -> Dict: """ Create dict with trailing stop parameters. """ @@ -190,8 +182,7 @@ class IHyperOpt(ABC): 'trailing_only_offset_is_reached': params['trailing_only_offset_is_reached'], } - @staticmethod - def trailing_space() -> List[Dimension]: + def trailing_space(self) -> List[Dimension]: """ Create a trailing stoploss space. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c06ae36da..a6603ecbf 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -101,7 +101,8 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, + optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. From d64295ba24a13958d4d79c7dd8dfb6134b83b85b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 06:55:25 +0200 Subject: [PATCH 040/460] Adapt test strategy to new parameters --- tests/strategy/strats/hyperoptable_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 97d2092d4..a08293058 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -62,7 +62,7 @@ class HyperoptableStrategy(IStrategy): buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') - sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', enabled=False) + sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) def informative_pairs(self): """ From 51f0fcb2cb071ae2753c94a21e23c87b9a57e129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 12:20:38 +0200 Subject: [PATCH 041/460] Add profit_fiat to REST API --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 32a1c8597..eaca477d7 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -168,6 +168,7 @@ class TradeSchema(BaseModel): profit_ratio: Optional[float] profit_pct: Optional[float] profit_abs: Optional[float] + profit_fiat: Optional[float] sell_reason: Optional[str] sell_order_status: Optional[str] stop_loss_abs: Optional[float] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 62f1c2592..1b2dc5d66 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -173,6 +173,14 @@ class RPC: current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) + + # Calculate fiat profit + current_profit_fiat = self._fiat_converter.convert_amount( + current_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) + # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss) @@ -191,6 +199,7 @@ class RPC: profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, + profit_fiat=current_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), From 2c0079b00b8902405f487f1c8ffbc1f614de8651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 13:16:52 +0200 Subject: [PATCH 042/460] Add profit_fiat to tests, use ANY, as price changes... --- tests/rpc/test_rpc_apiserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..3b1ef6a74 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -786,6 +786,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, + 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', From f47dc317867ebe5186519d99e4ee5d3b0ffab2d3 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:35 +0530 Subject: [PATCH 043/460] Refactor the comparison involving `not` Signed-off-by: shubhendra --- .deepsource.toml | 16 ---------------- freqtrade/exchange/exchange.py | 2 +- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- freqtrade/plugins/protections/cooldown_period.py | 1 - freqtrade/resolvers/strategy_resolver.py | 4 ++-- 5 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 7a00ca8d6..000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,16 +0,0 @@ -version = 1 - -test_patterns = ["tests/**/test_*.py"] - -exclude_patterns = [ - "docs/**", - "user_data/**", - "build/helpers/**" -] - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9c868df2b..85c5b4c6d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -806,7 +806,7 @@ class Exchange: # Gather coroutines to run for pair, timeframe in set(pair_list): - if (not ((pair, timeframe) in self._klines) + if (((pair, timeframe) not in self._klines) or self._now_is_time_to_refresh(pair, timeframe)): input_coroutines.append(self._async_get_candle_history(pair, timeframe, since_ms=since_ms)) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index c1355f655..73a9436fa 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Any, Dict, List +from typing import Dict, List import pandas as pd diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 197d74c2e..a2d8eca34 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,7 +1,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 diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 19bd014f9..05fbac10d 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -197,8 +197,8 @@ class StrategyResolver(IResolver): strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) if any(x == 2 for x in [strategy._populate_fun_len, - strategy._buy_fun_len, - strategy._sell_fun_len]): + strategy._buy_fun_len, + strategy._sell_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy From ede26091b980a22d84bc44ce5e5e3b74119ba76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 14:35:19 +0200 Subject: [PATCH 044/460] Add validation in the right places... --- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index b11470711..64918ed47 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -92,6 +92,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, + 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, @@ -159,6 +160,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, + 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3b1ef6a74..20d32024f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -966,6 +966,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'profit_ratio': None, 'profit_pct': None, 'profit_abs': None, + 'profit_fiat': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, From 3691ae8686d9987966956018aa8992b36278ce22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 14:50:47 +0200 Subject: [PATCH 045/460] Make sure the fiat converter exists before calling it --- freqtrade/rpc/rpc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1b2dc5d66..1359729b9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -175,11 +175,12 @@ class RPC: current_profit_abs = trade.calc_profit(current_rate) # Calculate fiat profit - current_profit_fiat = self._fiat_converter.convert_amount( - current_profit_abs, - self._freqtrade.config['stake_currency'], - self._freqtrade.config['fiat_display_currency'] - ) + if self._fiat_converter: + current_profit_fiat = self._fiat_converter.convert_amount( + current_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) From ea43d5ba85caf490604d02a6abae8e620324f034 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 1 Apr 2021 10:17:39 +0300 Subject: [PATCH 046/460] Implement DecimalParameter and rename FloatParameter to RealParameter. --- freqtrade/optimize/hyperopt_auto.py | 6 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 66 +++++++++++++++++-- .../strategy/strats/hyperoptable_strategy.py | 7 +- tests/strategy/test_interface.py | 26 ++++++-- 5 files changed, 88 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 0e3cf7eac..ed6f2d6f7 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -26,7 +26,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -35,7 +36,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index bc0c45f7c..bd49165df 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter, + RealParameter) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index a6603ecbf..e58aac273 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -56,6 +56,14 @@ class BaseParameter(ABC): Get-space - will be used by Hyperopt to get the hyperopt Space """ + def _set_value(self, value: Any): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: A numerical value. + """ + self.value = value + class IntParameter(BaseParameter): default: int @@ -65,7 +73,7 @@ class IntParameter(BaseParameter): def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable integer parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none of entire range is passed first parameter. @@ -95,16 +103,16 @@ class IntParameter(BaseParameter): return Integer(*self.opt_range, name=name, **self._space_params) -class FloatParameter(BaseParameter): +class RealParameter(BaseParameter): default: float value: float opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, - optimize: bool = True, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, + load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable floating point parameter with unlimited precision. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none if entire range is passed first parameter. @@ -116,10 +124,10 @@ class FloatParameter(BaseParameter): :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): - raise OperationalException('FloatParameter space invalid.') + raise OperationalException(f'{self.__class__.__name__} space invalid.') if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('FloatParameter space must be [low, high]') + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') opt_range = low else: opt_range = [low, high] @@ -134,6 +142,50 @@ class FloatParameter(BaseParameter): return Real(*self.opt_range, name=name, **self._space_params) +class DecimalParameter(RealParameter): + default: float + value: float + opt_range: Sequence[float] + + def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, + default: float, decimals: int = 3, space: Optional[str] = None, + optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable decimal parameter with a limited precision. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. + :param default: A default value. + :param decimals: A number of decimals after floating point to be included in testing. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.Real. + """ + self._decimals = decimals + default = round(default, self._decimals) + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, + load=load, **kwargs) + + def get_space(self, name: str) -> 'Integer': + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ + low = int(self.opt_range[0] * pow(10, self._decimals)) + high = int(self.opt_range[1] * pow(10, self._decimals)) + return Integer(low, high, name=name, **self._space_params) + + def _set_value(self, value: int): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: An integer value. + """ + self.value = round(value * pow(0.1, self._decimals), self._decimals) + + class CategoricalParameter(BaseParameter): default: Any value: Any diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index a08293058..cc4734e13 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import FloatParameter, IntParameter, IStrategy +from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, RealParameter class HyperoptableStrategy(IStrategy): @@ -60,9 +60,10 @@ class HyperoptableStrategy(IStrategy): } buy_rsi = IntParameter([0, 50], default=30, space='buy') - buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') - sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) def informative_pairs(self): """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4d93f7049..71f877cc3 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,8 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, - IntParameter) +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, + IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -564,14 +564,20 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): IntParameter(low=0, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): - FloatParameter(low=0, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space must be.*"): + RealParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space must be.*"): + DecimalParameter(low=0, default=5, space='buy') with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): IntParameter([0, 10], high=7, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): - FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space invalid\."): + RealParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space invalid\."): + DecimalParameter([0, 10], high=7, default=5, space='buy') with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): CategoricalParameter(['aa'], default='aa', space='buy') @@ -583,10 +589,16 @@ def test_hyperopt_parameters(): assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) - fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') + assert isinstance(fltpar.get_space(''), Integer) + assert fltpar.value == 1 + fltpar._set_value(2222) + assert fltpar.value == 2.222 + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') assert isinstance(catpar.get_space(''), Categorical) From 7728e269fdcae6b078913628a067edb5cbcdfd7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 07:17:02 +0200 Subject: [PATCH 047/460] Include Technical in default image --- docker-compose.yml | 2 +- docker/{Dockerfile.technical => Dockerfile.custom} | 3 ++- docs/docker_quickstart.md | 4 ++-- requirements.txt | 1 + setup.py | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) rename docker/{Dockerfile.technical => Dockerfile.custom} (50%) diff --git a/docker-compose.yml b/docker-compose.yml index 1f63059f0..80e194ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: # Build step - only needed when additional dependencies are needed # build: # context: . - # dockerfile: "./docker/Dockerfile.technical" + # dockerfile: "./docker/Dockerfile.custom" restart: unless-stopped container_name: freqtrade volumes: diff --git a/docker/Dockerfile.technical b/docker/Dockerfile.custom similarity index 50% rename from docker/Dockerfile.technical rename to docker/Dockerfile.custom index 9431e72d0..10620e6b8 100644 --- a/docker/Dockerfile.technical +++ b/docker/Dockerfile.custom @@ -3,4 +3,5 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/freqtrade/technical + # The below dependency - pyti - serves as an example. Please use whatever you need! + && pip install pyti diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 017264569..9e74841b4 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -156,8 +156,8 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) for an example). +If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.cusotm) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. diff --git a/requirements.txt b/requirements.txt index 93ed7570e..e4984cf47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 +technical==1.2.2 tabulate==0.8.9 pycoingecko==1.4.0 jinja2==2.11.3 diff --git a/setup.py b/setup.py index 118bc8485..bf23fb999 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ setup(name='freqtrade', 'wrapt', 'jsonschema', 'TA-Lib', + 'technical', 'tabulate', 'pycoingecko', 'py_find_1st', From e7a1924aa0e1c9c795516db10690af8fbccffb37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 08:36:06 +0200 Subject: [PATCH 048/460] Fix typo --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 9e74841b4..ca0515281 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -157,7 +157,7 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.cusotm) for an example). +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.custom) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. From 23c19b6852f62026f80366bde35ad7f8254f61b1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 3 Apr 2021 11:17:18 +0300 Subject: [PATCH 049/460] New hyperopt documentation. --- docs/hyperopt.md | 283 +++++++++--------- docs/hyperopt_legacy.md | 629 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 759 insertions(+), 153 deletions(-) create mode 100644 docs/hyperopt_legacy.md diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 69bc57d1a..e9d440a5a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -14,6 +14,9 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) +!!! Note + Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + ## Install hyperopt dependencies Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. @@ -137,47 +140,19 @@ Strategy arguments: ``` -## Prepare Hyperopting - -Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). - -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - ### Hyperopt checklist Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization +* define parameters with `space='buy'` - for buy signal optimization +* define parameters with `space='sell'` - for sell signal optimization !!! Note `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `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. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: +Rarely you may also need to create a nested class named `HyperOpt` and implement: * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) @@ -185,31 +160,19 @@ Rarely you may also need to override: * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" - You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. + You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. ```python # Have a working strategy at hand. - freqtrade new-hyperopt --hyperopt EmptyHyperopt - - freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` -### Create a Custom Hyperopt File - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -This command will create a new hyperopt file from a template, allowing you to get started quickly. - ### Configure your Guards and Triggers -There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: +There are two places you need to change in your strategy file to add a new buy hyperopt for testing: -* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. +* Define the parameters at the class level hyperopt shall be optimizing. +* Within `populate_buy_trend()` - use defined parameter values instead of raw constants. There you have two different types of indicators: 1. `guards` and 2. `triggers`. @@ -225,24 +188,46 @@ Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if ADX > 10*". -If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. +```python +from freqtrade.strategy import IntParameter, IStrategy + +class MyAwesomeStrategy(IStrategy): + # If parameter is prefixed with `buy_` or `sell_` then specifying `space` parameter is optional + # and space is inferred from parameter name. + buy_adx_min = IntParameter(0, 100, default=10) + + def populate_buy_trend(self, dataframe: 'DataFrame', metadata: dict) -> 'DataFrame': + dataframe.loc[ + ( + (dataframe['adx'] > self.buy_adx_min.value) + ), 'buy'] = 1 + return dataframe +``` #### Sell optimization 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. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. +* Define the parameters at the class level hyperopt shall be optimizing. +* Within `populate_sell_trend()` - use defined parameter values instead of raw constants. 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-`. -#### Using timeframe as a part of the Strategy +```python +class MyAwesomeStrategy(IStrategy): + # There is no strict parameter naming scheme. If you do not use `buy_` or `sell_` prefixes - + # please specify to which space parameter belongs using `space` parameter. Possible values: + # 'buy' or 'sell'. + adx_max = IntParameter(0, 100, default=50, space='sell') -The Strategy class exposes the timeframe value as the `self.timeframe` attribute. -The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['adx'] < self.adx_max.value) + ), 'buy'] = 1 + return dataframe +``` ## Solving a Mystery @@ -252,25 +237,20 @@ help with those buy decisions. If you decide to use RSI or ADX, which values should I use for them? So let's use hyperparameter optimization to solve this mystery. -We will start by defining a search space: +We will start by defining hyperoptable parameters: ```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] +class MyAwesomeStrategy(IStrategy): + buy_adx = IntParameter(20, 40, default=30) + buy_rsi = IntParameter(20, 40, default=30) + buy_adx_enabled = CategoricalParameter([True, False]), + buy_rsi_enabled = CategoricalParameter([True, False]), + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), ``` Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +to find the best combination. Two of them are integer values (`buy_adx` +and `buy_rsi`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. @@ -278,39 +258,31 @@ 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, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if self.buy_adx_enabled.value: + conditions.append(dataframe['adx'] > self.buy_adx.value) + if self.buy_rsi_enabled.value: + conditions.append(dataframe['rsi'] < self.buy_rsi.value) - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) + # TRIGGERS + if self.buy_trigger.value == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if self.buy_trigger.value == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 - return dataframe - - return populate_buy_trend + return dataframe ``` Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. @@ -322,6 +294,20 @@ Based on the results, hyperopt will tell you which parameter combination produce When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in your strategy or hyperopt file. +## Parameter types + +There are four parameter types each suited for different purposes. +* `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. +* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. +* `RealParameter` - defines a floating point parameter with upper and lower boundarie and no precision limit. Rarely used. +* `CategoricalParameter` - defines a parameter with a predetermined number of choices. + +!!! Tip "Disabling parameter optimization" + Each parameter takes two boolean parameters: + * `load` - when set to `False` it will not load values configured in `buy_params` and `sell_params`. + * `optimize` - when set to `False` parameter will not be included in optimization process. + Use these parameters to quickly prototype various ideas. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. @@ -348,11 +334,9 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +freqtrade hyperopt --config config.json --hyperopt-loss --strategy -e 500 --spaces all ``` -Use `` as the name of the custom hyperopt used. - The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. @@ -378,14 +362,6 @@ For example, to use one month of data, pass the following parameter to the hyper freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 ``` -### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. @@ -439,7 +415,7 @@ If you have not changed anything in the command line options, configuration, tim ## Understand the Hyperopt Result -Once Hyperopt is completed you can use the result to create a new strategy. +Once Hyperopt is completed you can use the result to update your strategy. Given the following result from hyperopt: ``` @@ -447,40 +423,36 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } ``` You should understand this result like: - The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) -- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) +- You should not use ADX because `'buy_adx_enabled': False`) +- You should **consider** using the RSI indicator (`'buy_rsi_enabled': True` and the best value is `29.0` (`'buy_rsi': 29.0`) -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. +Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste it at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: +Transferring your whole hyperopt result to your strategy would then look like: ```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe +class MyAwsomeStrategy(IStrategy): + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } ``` By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. @@ -499,11 +471,13 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -ROI table: -{ 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0} + # ROI table: + minimal_roi = { + 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0 + } ``` In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: @@ -549,13 +523,16 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -Stoploss: -0.27996 + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } + + stoploss: -0.27996 ``` In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: @@ -585,11 +562,11 @@ Best result: 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 -Trailing stop: -{ 'trailing_only_offset_is_reached': True, - 'trailing_stop': True, - 'trailing_stop_positive': 0.02001, - 'trailing_stop_positive_offset': 0.06038} + # Trailing stop: + trailing_stop = True + trailing_stop_positive = 0.02001 + trailing_stop_positive_offset = 0.06038 + trailing_only_offset_is_reached = True ``` In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md new file mode 100644 index 000000000..8c6972b5f --- /dev/null +++ b/docs/hyperopt_legacy.md @@ -0,0 +1,629 @@ +# Legacy Hyperopt + +This page explains how to tune your strategy by finding the optimal +parameters, a process called hyperparameter optimization. The bot uses several +algorithms included in the `scikit-optimize` package to accomplish this. The +search will burn all your CPU cores, make your laptop sound like a fighter jet +and still take a long time. + +In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). + +Hyperopt requires historic data to be available, just as backtesting does. +To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. + +!!! Note + Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Please update your strategy class following new documentation at [Hyperopt](hyperopt.md). + +!!! Bug +Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) + +## Install hyperopt dependencies + +Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. + +!!! Note +Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported. + +### Docker + +The docker-image includes hyperopt dependencies, no further action needed. + +### Easy installation script (setup.sh) / Manual installation + +```bash +source .env/bin/activate +pip install -r requirements-hyperopt.txt +``` + +## Hyperopt command reference + + +``` +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] + [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [-e INT] + [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] + [--print-all] [--no-color] [--print-json] [-j JOBS] + [--random-state INT] [--min-trades INT] + [--hyperopt-loss NAME] + +optional arguments: + -h, --help show this help message and exit + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `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. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --hyperopt-path PATH Specify additional lookup path for Hyperopt and + Hyperopt Loss functions. + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking). + --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 + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. + -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. + --print-json Print output in JSON format. + -j JOBS, --job-workers JOBS + The number of concurrently running jobs for + hyperoptimization (hyperopt worker processes). If -1 + (default), all CPUs are used, for -2, all CPUs but one + are used, etc. If 1 is given, no parallel computing + code is used at all. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --hyperopt-loss NAME Specify the class name of the hyperopt loss function + 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 + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + +## Prepare Hyperopting + +Before we start digging into Hyperopt, we recommend you to take a look at +the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). + +Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. + +!!! Tip "About this page" +For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + +The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +### Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt + +Depending on the space you want to optimize, only some of the below are required: + +* fill `buy_strategy_generator` - for buy signal optimization +* fill `indicator_space` - for buy signal optimization +* fill `sell_strategy_generator` - for sell signal optimization +* fill `sell_indicator_space` - for sell signal optimization + +!!! Note +`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + +Optional in hyperopt - can also be loaded from a strategy (recommended): + +* `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. +Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) +* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) + +!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" +You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. + + ```python + # Have a working strategy at hand. + freqtrade new-hyperopt --hyperopt EmptyHyperopt + + freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + ``` + +### Create a Custom Hyperopt File + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +This command will create a new hyperopt file from a template, allowing you to get started quickly. + +### Configure your Guards and Triggers + +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. +* 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`. + +1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. +2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". + +!!! Hint "Guards and Triggers" +Technically, there is no difference between Guards and Triggers. +However, this guide will make this distinction to make it clear that signals should not be "sticking". +Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). + +Hyper-optimization will, for each epoch round, pick one trigger and possibly +multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if +ADX > 10*". + +If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. + +#### Sell optimization + +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. +* 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-`. + +#### Using timeframe as a part of the Strategy + +The Strategy class exposes the timeframe value as the `self.timeframe` attribute. +The same value is available as class-attribute `HyperoptName.timeframe`. +In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. + +## Solving a Mystery + +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this +mystery. + +We will start by defining a search space: + +```python + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(20, 40, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') + ] +``` + +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` +and `rsi-value`) and I want you test in the range of values 20 to 40. +Then we have three category variables. First two are either `True` or `False`. +We use these to either enable or disable the ADX and RSI guards. The last +one we call `trigger` and use it to decide which buy trigger we want to use. + +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, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend +``` + +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). + +!!! Note +The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. +When you want to test an indicator that isn't used by the bot currently, remember to +add it to the `populate_indicators()` method in your strategy or hyperopt file. + +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. + +A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). +This class should be in its own file within the `user_data/hyperopts/` directory. + +Currently, the following loss functions are builtin: + +* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) +* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) +* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) +* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) +* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) + +Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. + +## Execute Hyperopt + +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + +```bash +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +``` + +Use `` as the name of the custom hyperopt used. + +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. + +The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. + +!!! Note +Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. +Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. +You can find a list of filenames with `ls -l user_data/hyperopt_results/`. + +### Execute Hyperopt with different historical data source + +If you would like to hyperopt parameters using an alternate historical data set that +you have on-disk, use the `--datadir PATH` option. By default, hyperopt +uses data from directory `user_data/data`. + +### Running Hyperopt with a smaller test-set + +Use the `--timerange` argument to change how much of the test-set you want to use. +For example, to use one month of data, pass the following parameter to the hyperopt call: + +```bash +freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 +``` + +### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy +``` + +### Running Hyperopt with Smaller Search Space + +Use the `--spaces` option to limit the search space used by hyperopt. +Letting Hyperopt optimize everything is a huuuuge search space. +Often it might make more sense to start by just searching for initial buy algorithm. +Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. + +Legal values are: + +* `all`: optimize everything +* `buy`: just search for a new buy strategy +* `sell`: just search for a new sell strategy +* `roi`: just optimize the minimal profit table for your strategy +* `stoploss`: search for the best stoploss value +* `trailing`: search for the best trailing stop values +* `default`: `all` except `trailing` +* space-separated list of any of the above values for example `--spaces roi stoploss` + +The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. + +### Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previously open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note +Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. + +## Understand the Hyperopt Result + +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +``` + +You should understand this result like: + +- The buy trigger that worked best was `bb_lower`. +- You should not use ADX because `adx-enabled: False`) +- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) + +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. + +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + +```python +(dataframe['rsi'] < 29.0) +``` + +Translating your whole hyperopt result as the new buy-signal would then look like: + +```python +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 29.0) & # rsi-value + dataframe['close'] < dataframe['bb_lowerband'] # trigger + ), + 'buy'] = 1 + return dataframe +``` + +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. + +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. + +!!! Note "Windows and color output" +Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + +### Understand Hyperopt ROI results + +If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +ROI table: +{ 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0} +``` + +In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: + +``` + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0 + } +``` + +As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file. + +#### Default ROI Search Space + +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): + +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | +| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | +| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | +| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | + +These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. + +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. + +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). + +A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +### Understand Hyperopt Stoploss results + +If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +Stoploss: -0.27996 +``` + +In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: + +``` python + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.27996 +``` + +As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. + +#### Default Stoploss Search Space + +If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases. + +If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +### Understand Hyperopt Trailing Stop results + +If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: + +``` +Best result: + + 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 + +Trailing stop: +{ 'trailing_only_offset_is_reached': True, + 'trailing_stop': True, + 'trailing_stop_positive': 0.02001, + 'trailing_stop_positive_offset': 0.06038} +``` + +In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: + +``` python + # Trailing stop + # These attributes will be overridden if the config file contains corresponding values. + trailing_stop = True + trailing_stop_positive = 0.02001 + trailing_stop_positive_offset = 0.06038 + trailing_only_offset_is_reached = True +``` + +As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file. + +#### Default Trailing Stop Search Space + +If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. + +Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +## Show details of Hyperopt results + +After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. + +## Validate backtesting results + +Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. + +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From faf40482ef9bebf9cbb5469f17b4d136fded3ced Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 3 Apr 2021 13:49:24 +0300 Subject: [PATCH 050/460] Fix parameter printing. --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e58aac273..6282d91c0 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -259,7 +259,7 @@ class HyperStrategyMixin(object): if attr_name in params: if attr.load: attr.value = params[attr_name] - logger.info(f'attr_name = {attr.value}') + logger.info(f'{attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') From 4eb7ce52cd61fcd14f546b72be3b45ee03bcc8a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 15:15:38 +0200 Subject: [PATCH 051/460] Remove duplicate entries from hyperopt_legacy --- docs/hyperopt.md | 47 ++--- docs/hyperopt_legacy.md | 446 ++++------------------------------------ 2 files changed, 62 insertions(+), 431 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e9d440a5a..725afebf3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,21 +1,21 @@ # Hyperopt This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several -algorithms included in the `scikit-optimize` package to accomplish this. The -search will burn all your CPU cores, make your laptop sound like a fighter jet -and still take a long time. +parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this. +The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). -Hyperopt requires historic data to be available, just as backtesting does. +Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters). To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) !!! Note - Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. + The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. + The legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). ## Install hyperopt dependencies @@ -37,7 +37,6 @@ pip install -r requirements-hyperopt.txt ## Hyperopt command reference - ``` usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] @@ -150,7 +149,7 @@ Depending on the space you want to optimize, only some of the below are required * define parameters with `space='sell'` - for sell signal optimization !!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. Rarely you may also need to create a nested class named `HyperOpt` and implement: @@ -299,7 +298,7 @@ Based on the results, hyperopt will tell you which parameter combination produce There are four parameter types each suited for different purposes. * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. -* `RealParameter` - defines a floating point parameter with upper and lower boundarie and no precision limit. Rarely used. +* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. * `CategoricalParameter` - defines a parameter with a predetermined number of choices. !!! Tip "Disabling parameter optimization" @@ -329,7 +328,7 @@ Creation of a custom loss function is covered in the [Advanced Hyperopt](advance ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. We strongly recommend to use `screen` or `tmux` to prevent any connection loss. @@ -365,7 +364,7 @@ freqtrade hyperopt --hyperopt --strategy --timeran ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. +Letting Hyperopt optimize everything is a huuuuge search space. Often it might make more sense to start by just searching for initial buy algorithm. Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. @@ -435,16 +434,16 @@ Best result: You should understand this result like: -- The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `'buy_adx_enabled': False`) -- You should **consider** using the RSI indicator (`'buy_rsi_enabled': True` and the best value is `29.0` (`'buy_rsi': 29.0`) +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `'buy_adx_enabled': False`. +* You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`) -Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste it at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. +Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. Transferring your whole hyperopt result to your strategy would then look like: ```python -class MyAwsomeStrategy(IStrategy): +class MyAwesomeStrategy(IStrategy): # Buy hyperspace params: buy_params = { 'buy_adx': 44, @@ -455,13 +454,6 @@ class MyAwsomeStrategy(IStrategy): } ``` -By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. - -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. - -!!! Note "Windows and color output" - Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - ### Understand Hyperopt ROI results If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: @@ -588,6 +580,15 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +### Output formatting + +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. + +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. + +!!! Note "Windows and color output" + Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md index 8c6972b5f..03c1eb358 100644 --- a/docs/hyperopt_legacy.md +++ b/docs/hyperopt_legacy.md @@ -1,162 +1,29 @@ # Legacy Hyperopt -This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several -algorithms included in the `scikit-optimize` package to accomplish this. The -search will burn all your CPU cores, make your laptop sound like a fighter jet -and still take a long time. +This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). -In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). +!!! Warning "Deprecated / legacy mode" + Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. + Please read the [main hyperopt page](hyperopt.md) for more details. -Hyperopt requires historic data to be available, just as backtesting does. -To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. +## Prepare hyperopt file -!!! Note - Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Please update your strategy class following new documentation at [Hyperopt](hyperopt.md). - -!!! Bug -Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) - -## Install hyperopt dependencies - -Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. - -!!! Note -Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported. - -### Docker - -The docker-image includes hyperopt dependencies, no further action needed. - -### Easy installation script (setup.sh) / Manual installation - -```bash -source .env/bin/activate -pip install -r requirements-hyperopt.txt -``` - -## Hyperopt command reference - - -``` -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] - [--dmmp] [--enable-protections] - [--dry-run-wallet DRY_RUN_WALLET] [-e INT] - [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] - [--print-all] [--no-color] [--print-json] [-j JOBS] - [--random-state INT] [--min-trades INT] - [--hyperopt-loss NAME] - -optional arguments: - -h, --help show this help message and exit - -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `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. - --stake-amount STAKE_AMOUNT - Override the value of the `stake_amount` configuration - setting. - --fee FLOAT Specify fee ratio. Will be applied twice (on trade - entry and exit). - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. - --eps, --enable-position-stacking - Allow buying the same pair multiple times (position - stacking). - --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 - --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET - Starting balance, used for backtesting / hyperopt and - dry-runs. - -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. - --print-json Print output in JSON format. - -j JOBS, --job-workers JOBS - The number of concurrently running jobs for - hyperoptimization (hyperopt worker processes). If -1 - (default), all CPUs are used, for -2, all CPUs but one - are used, etc. If 1 is given, no parallel computing - code is used at all. - --random-state INT Set random state to some positive integer for - reproducible hyperopt results. - --min-trades INT Set minimal desired number of trades for evaluations - in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function - 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 - -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: - `userdir/config.json` or `config.json` whichever - exists). Multiple --config options may be used. Can be - set to `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - -``` - -## Prepare Hyperopting - -Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). - -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. +Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. !!! Tip "About this page" -For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. -The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. +### Create a Custom Hyperopt File + +The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: ``` bash freqtrade new-hyperopt --hyperopt AwesomeHyperopt ``` -### Hyperopt checklist +### Legacy Hyperopt checklist Checklist on all tasks / possibilities in hyperopt @@ -168,7 +35,7 @@ Depending on the space you want to optimize, only some of the below are required * fill `sell_indicator_space` - for sell signal optimization !!! Note -`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. Optional in hyperopt - can also be loaded from a strategy (recommended): @@ -177,8 +44,8 @@ Optional in hyperopt - can also be loaded from a strategy (recommended): * `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. -Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. Rarely you may also need to override: @@ -187,67 +54,7 @@ Rarely you may also need to override: * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) -!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" -You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. - - ```python - # Have a working strategy at hand. - freqtrade new-hyperopt --hyperopt EmptyHyperopt - - freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 - ``` - -### Create a Custom Hyperopt File - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -This command will create a new hyperopt file from a template, allowing you to get started quickly. - -### Configure your Guards and Triggers - -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. -* 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`. - -1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. -2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". - -!!! Hint "Guards and Triggers" -Technically, there is no difference between Guards and Triggers. -However, this guide will make this distinction to make it clear that signals should not be "sticking". -Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). - -Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if -ADX > 10*". - -If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. - -#### Sell optimization - -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. -* 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-`. - -#### Using timeframe as a part of the Strategy - -The Strategy class exposes the timeframe value as the `self.timeframe` attribute. -The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. - -## Solving a Mystery +### Defining a buy signal optimization Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys. And you also wonder should you use RSI or ADX to @@ -272,13 +79,12 @@ We will start by defining a search space: ``` Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. The last -one we call `trigger` and use it to decide which buy trigger we want to use. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. -So let's write the buy strategy using these values: +So let's write the buy strategy generator using these values: ```python @staticmethod @@ -321,27 +127,20 @@ It will use the given historical data and make buys based on the buy signals gen Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). !!! Note -The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. -When you want to test an indicator that isn't used by the bot currently, remember to -add it to the `populate_indicators()` method in your strategy or hyperopt file. + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + When you want to test an indicator that isn't used by the bot currently, remember to + add it to the `populate_indicators()` method in your strategy or hyperopt file. -## Loss-functions +### Sell optimization -Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods -A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). -This class should be in its own file within the `user_data/hyperopts/` directory. +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. -Currently, the following loss functions are builtin: - -* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. -* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) -* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) -* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) -* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) -* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) - -Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. +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-`. ## Execute Hyperopt @@ -362,24 +161,9 @@ Doing multiple runs (executions) with a few 1000 epochs and different random sta The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. !!! Note -Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. -Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. -You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -### Execute Hyperopt with different historical data source - -If you would like to hyperopt parameters using an alternate historical data set that -you have on-disk, use the `--datadir PATH` option. By default, hyperopt -uses data from directory `user_data/data`. - -### Running Hyperopt with a smaller test-set - -Use the `--timerange` argument to change how much of the test-set you want to use. -For example, to use one month of data, pass the following parameter to the hyperopt call: - -```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 -``` + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. ### Running Hyperopt using methods from a strategy @@ -389,57 +173,6 @@ Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_t freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy ``` -### Running Hyperopt with Smaller Search Space - -Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. -Often it might make more sense to start by just searching for initial buy algorithm. -Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. - -Legal values are: - -* `all`: optimize everything -* `buy`: just search for a new buy strategy -* `sell`: just search for a new sell strategy -* `roi`: just optimize the minimal profit table for your strategy -* `stoploss`: search for the best stoploss value -* `trailing`: search for the best trailing stop values -* `default`: `all` except `trailing` -* space-separated list of any of the above values for example `--spaces roi stoploss` - -The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. - -### Position stacking and disabling max market positions - -In some situations, you may need to run Hyperopt (and Backtesting) with the -`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. - -By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one -open trade is allowed for every traded pair. The total number of trades open for all pairs -is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to -some potential trades to be hidden (or masked) by previously open trades. - -The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, -while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high -number). - -!!! Note -Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. - -You can also enable position stacking in the configuration file by explicitly setting -`"position_stacking"=true`. - -### Reproducible results - -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. - -The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. - -If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. - -If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. - ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. @@ -460,9 +193,9 @@ Buy hyperspace params: You should understand this result like: -- The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) -- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `adx-enabled: False`) +* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) You have to look inside your strategy file into `buy_strategy_generator()` method, what those values match to. @@ -486,63 +219,6 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: return dataframe ``` -By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. - -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. - -!!! Note "Windows and color output" -Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - -### Understand Hyperopt ROI results - -If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -ROI table: -{ 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0} -``` - -In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: - -``` - # Minimal ROI designed for the strategy. - # This attribute will be overridden if the config file contains "minimal_roi" - minimal_roi = { - 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0 - } -``` - -As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file. - -#### Default ROI Search Space - -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): - -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | - -These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. - -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. - -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). - -A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -571,56 +247,10 @@ In order to use this best stoploss value found by Hyperopt in backtesting and fo As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. -#### Default Stoploss Search Space - -If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases. - -If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. - -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - -### Understand Hyperopt Trailing Stop results - -If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: - -``` -Best result: - - 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 - -Trailing stop: -{ 'trailing_only_offset_is_reached': True, - 'trailing_stop': True, - 'trailing_stop_positive': 0.02001, - 'trailing_stop_positive_offset': 0.06038} -``` - -In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: - -``` python - # Trailing stop - # These attributes will be overridden if the config file contains corresponding values. - trailing_stop = True - trailing_stop_positive = 0.02001 - trailing_stop_positive_offset = 0.06038 - trailing_only_offset_is_reached = True -``` - -As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file. - -#### Default Trailing Stop Search Space - -If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. - -Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - -## Show details of Hyperopt results - -After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. ## Validate backtesting results -Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. +Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. From 32a503491d76fa84f4c4823095b279eff0515bfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 15:41:43 +0200 Subject: [PATCH 052/460] Reorder hyperopt methods --- docs/hyperopt.md | 70 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 725afebf3..806ce3c94 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -355,10 +355,12 @@ uses data from directory `user_data/data`. ### Running Hyperopt with a smaller test-set Use the `--timerange` argument to change how much of the test-set you want to use. -For example, to use one month of data, pass the following parameter to the hyperopt call: +For example, to use one month of data, pass `--timerange 20210101-20210201` (from january 2021 - february 2021) to the hyperopt call. + +Full command: ```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 +freqtrade hyperopt --hyperopt --strategy --timerange 20210101-20210201 ``` ### Running Hyperopt with Smaller Search Space @@ -381,37 +383,6 @@ Legal values are: The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. -### Position stacking and disabling max market positions - -In some situations, you may need to run Hyperopt (and Backtesting) with the -`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. - -By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one -open trade is allowed for every traded pair. The total number of trades open for all pairs -is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to -some potential trades to be hidden (or masked) by previously open trades. - -The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, -while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high -number). - -!!! Note - Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. - -You can also enable position stacking in the configuration file by explicitly setting -`"position_stacking"=true`. - -### Reproducible results - -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. - -The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. - -If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. - -If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. - ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to update your strategy. @@ -580,7 +551,17 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). -### Output formatting +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. + +## Output formatting By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. @@ -589,6 +570,27 @@ You can use the `--print-all` command line option if you would like to see all r !!! Note "Windows and color output" Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. +## Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previously open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. From c2d43a526cf5f0e96f016851b2b74eb814c9ac74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:08:08 +0200 Subject: [PATCH 053/460] Combine Legacy and advanced hyperopt sections --- docs/advanced-hyperopt.md | 349 ++++++++++++++++++++++++++++++-------- docs/hyperopt.md | 14 +- docs/hyperopt_legacy.md | 259 ---------------------------- 3 files changed, 282 insertions(+), 340 deletions(-) delete mode 100644 docs/hyperopt_legacy.md diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index bdaafb936..6a559ec96 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -4,79 +4,6 @@ This page explains some advanced Hyperopt topics that may require higher coding skills and Python knowledge than creation of an ordinal hyperoptimization class. -## Derived hyperopt classes - -Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). - -Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace: - -```python -class MyAwesomeHyperOpt(IHyperOpt): - ... - # Uses default stoploss dimension - -class MyAwesomeHyperOpt2(MyAwesomeHyperOpt): - @staticmethod - def stoploss_space() -> List[Dimension]: - # Override boundaries for stoploss - return [ - Real(-0.33, -0.01, name='stoploss'), - ] -``` - -and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case: - -``` -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... -or -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... -``` - -## Sharing methods with your strategy - -Hyperopt classes provide access to the Strategy via the `strategy` class attribute. -This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. - -``` python -from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy -import freqtrade.vendor.qtpylib.indicators as qtpylib - -class MyAwesomeStrategy(IStrategy): - - buy_params = { - 'rsi-value': 30, - 'adx-value': 35, - } - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - return self.buy_strategy_generator(self.buy_params, dataframe, metadata) - - @staticmethod - def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & - dataframe['adx'] > params['adx-value']) & - dataframe['volume'] > 0 - ) - , 'buy'] = 1 - return dataframe - -class MyAwesomeHyperOpt(IHyperOpt): - ... - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - # Call strategy's buy strategy generator - return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) - - return populate_buy_trend -``` - ## Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. @@ -142,3 +69,279 @@ This function needs to return a floating point number (`float`). Smaller numbers !!! Note Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. + +## Legacy Hyperopt + +This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). + +!!! Warning "Deprecated / legacy mode" + Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. + Please read the [main hyperopt page](hyperopt.md) for more details. + +### Prepare hyperopt file + +Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. + +!!! Tip "About this page" + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + +#### Create a Custom Hyperopt File + +The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +#### Legacy Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt + +Depending on the space you want to optimize, only some of the below are required: + +* fill `buy_strategy_generator` - for buy signal optimization +* fill `indicator_space` - for buy signal optimization +* fill `sell_strategy_generator` - for sell signal optimization +* fill `sell_indicator_space` - for sell signal optimization + +!!! Note + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + +Optional in hyperopt - can also be loaded from a strategy (recommended): + +* `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. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) +* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) + +#### Defining a buy signal optimization + +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this +mystery. + +We will start by defining a search space: + +```python + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(20, 40, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') + ] +``` + +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. +Then we have three category variables. First two are either `True` or `False`. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. + +So let's write the buy strategy generator 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, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend +``` + +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). + +!!! Note + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + When you want to test an indicator that isn't used by the bot currently, remember to + add it to the `populate_indicators()` method in your strategy or hyperopt file. + +#### Sell optimization + +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. +* 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-`. + +### Execute Hyperopt + +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + +```bash +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +``` + +Use `` as the name of the custom hyperopt used. + +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. + +The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. + +!!! Note + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. + +#### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy +``` + +### Understand the Hyperopt Result + +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +``` + +You should understand this result like: + +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `adx-enabled: False`) +* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) + +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. + +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + +```python +(dataframe['rsi'] < 29.0) +``` + +Translating your whole hyperopt result as the new buy-signal would then look like: + +```python +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 29.0) & # rsi-value + dataframe['close'] < dataframe['bb_lowerband'] # trigger + ), + 'buy'] = 1 + return dataframe +``` + +### Validate backtesting results + +Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. + +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). + +### Sharing methods with your strategy + +Hyperopt classes provide access to the Strategy via the `strategy` class attribute. +This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. + +``` python +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + + buy_params = { + 'rsi-value': 30, + 'adx-value': 35, + } + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return self.buy_strategy_generator(self.buy_params, dataframe, metadata) + + @staticmethod + def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & + dataframe['adx'] > params['adx-value']) & + dataframe['volume'] > 0 + ) + , 'buy'] = 1 + return dataframe + +class MyAwesomeHyperOpt(IHyperOpt): + ... + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + # Call strategy's buy strategy generator + return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) + + return populate_buy_trend +``` diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 806ce3c94..cb8d4ad9d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -15,7 +15,7 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Note Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. - The legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + The legacy documentation is available at [Legacy Hyperopt](advanced-hyperopt.md#legacy-hyperopt). ## Install hyperopt dependencies @@ -247,12 +247,11 @@ class MyAwesomeStrategy(IStrategy): buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), ``` -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`buy_adx` -and `buy_rsi`) and I want you test in the range of values 20 to 40. +Above definition says: I have five parameters I want to randomly combine to find the best combination. +Two of them are integer values (`buy_adx` and `buy_rsi`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. The last -one we call `trigger` and use it to decide which buy trigger we want to use. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: @@ -349,8 +348,7 @@ The `--spaces all` option determines that all possible parameters should be opti ### Execute Hyperopt with different historical data source If you would like to hyperopt parameters using an alternate historical data set that -you have on-disk, use the `--datadir PATH` option. By default, hyperopt -uses data from directory `user_data/data`. +you have on-disk, use the `--datadir PATH` option. By default, hyperopt uses data from directory `user_data/data`. ### Running Hyperopt with a smaller test-set diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md deleted file mode 100644 index 03c1eb358..000000000 --- a/docs/hyperopt_legacy.md +++ /dev/null @@ -1,259 +0,0 @@ -# Legacy Hyperopt - -This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). - -!!! Warning "Deprecated / legacy mode" - Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. - Please read the [main hyperopt page](hyperopt.md) for more details. - -## Prepare hyperopt file - -Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -### Create a Custom Hyperopt File - -The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -### Legacy Hyperopt checklist - -Checklist on all tasks / possibilities in hyperopt - -Depending on the space you want to optimize, only some of the below are required: - -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization - -!!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. - -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `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. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: - -* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) -* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) -* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) -* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) - -### Defining a buy signal optimization - -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining a search space: - -```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] -``` - -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. -Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. -The last one we call `trigger` and use it to decide which buy trigger we want to use. - -So let's write the buy strategy generator 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, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend -``` - -Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. -Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). - -!!! Note - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. - When you want to test an indicator that isn't used by the bot currently, remember to - add it to the `populate_indicators()` method in your strategy or hyperopt file. - -### Sell optimization - -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. -* 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-`. - -## Execute Hyperopt - -Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. - -We strongly recommend to use `screen` or `tmux` to prevent any connection loss. - -```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all -``` - -Use `` as the name of the custom hyperopt used. - -The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. - -The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. - -!!! Note - Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. - Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. - You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - -## Understand the Hyperopt Result - -Once Hyperopt is completed you can use the result to create a new strategy. -Given the following result from hyperopt: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -``` - -You should understand this result like: - -* The buy trigger that worked best was `bb_lower`. -* You should not use ADX because `adx-enabled: False`) -* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) - -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. - -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: - -```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe -``` - -### Understand Hyperopt Stoploss results - -If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -Stoploss: -0.27996 -``` - -In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: - -``` python - # Optimal stoploss designed for the strategy - # This attribute will be overridden if the config file contains "stoploss" - stoploss = -0.27996 -``` - -As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. - - -## Validate backtesting results - -Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. - -To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. - -Should results don't match, please double-check to make sure you transferred all conditions correctly. -Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. -You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From 093d6ce8af24bdf37a14b0505f23dd414d40682f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:13:49 +0200 Subject: [PATCH 054/460] Add sample for Nested space --- docs/advanced-hyperopt.md | 12 ++++++++++++ docs/hyperopt.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 6a559ec96..723163b2c 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -70,6 +70,18 @@ This function needs to return a floating point number (`float`). Smaller numbers !!! Note Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. +## Overriding pre-defined spaces + +To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows: + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + # Define a custom stoploss space. + def stoploss_space(self): + return [Real(-0.05, -0.01, name='stoploss')] +``` + ## Legacy Hyperopt This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index cb8d4ad9d..1ce1f9a86 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -151,7 +151,7 @@ Depending on the space you want to optimize, only some of the below are required !!! Note `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. -Rarely you may also need to create a nested class named `HyperOpt` and implement: +Rarely you may also need to create a [nested class](advanced-hyperopt.md#overriding-pre-defined-spaces) named `HyperOpt` and implement * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) From 771fc057494a55902624e633d9ff8066eb1da323 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:32:16 +0200 Subject: [PATCH 055/460] Update sample strategy with hyperoptable Parameters --- freqtrade/templates/base_strategy.py.j2 | 4 +++- freqtrade/templates/sample_strategy.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index dd6b773e1..9d69ee520 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 # --- Do not remove these libs --- import numpy as np # noqa @@ -6,6 +7,7 @@ import pandas as pd # noqa from pandas import DataFrame from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter # -------------------------------- # Add your lib to import here @@ -16,7 +18,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class {{ strategy }}(IStrategy): """ This is a strategy template to get you started. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + More information in https://www.freqtrade.io/en/latest/strategy-customization/ You can: :return: a Dataframe with all mandatory indicators for the strategies diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index db1ba48b8..84f3fbc9e 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,11 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 # isort: skip_file # --- Do not remove these libs --- import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter # -------------------------------- # Add your lib to import here @@ -52,7 +54,11 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. + # Hyperoptable parameters + buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, defualt=70, space='buy', optimize=True, load=True) + + # Optimal timeframe for the strategy. timeframe = '5m' # Run "populate_indicators()" only for new candle. @@ -339,7 +345,8 @@ class SampleStrategy(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 @@ -357,7 +364,8 @@ class SampleStrategy(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + # Signal: RSI crosses above 70 + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 From 6555454bd287a230de64e9686a09f6bf70fd36fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:54:47 +0200 Subject: [PATCH 056/460] Remove more ticker_interval occurances --- docs/backtesting.md | 3 +-- docs/edge.md | 3 +-- docs/hyperopt.md | 3 +-- docs/plotting.md | 6 ++---- docs/utils.md | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/commands/list_commands.py | 2 +- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/templates/sample_strategy.py | 2 +- tests/optimize/test_backtest_detail.py | 2 +- tests/strategy/strats/default_strategy.py | 2 +- tests/strategy/strats/legacy_strategy.py | 2 +- 12 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index d02c59f05..c8acfdbe1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -23,8 +23,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} diff --git a/docs/edge.md b/docs/edge.md index 0aa76cd12..7f0a9cb2d 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -221,8 +221,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --max-open-trades INT diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 96c7354b9..7ae06660b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -53,8 +53,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} diff --git a/docs/plotting.md b/docs/plotting.md index d7ed5ab1f..63afa16b6 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -66,8 +66,7 @@ optional arguments: --timerange TIMERANGE Specify what timerange of data to use. -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --no-trades Skip using trades from backtesting file and DB. Common arguments: @@ -264,8 +263,7 @@ optional arguments: Specify the source for trades (Can be DB or file (backtest file)) Default: file -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/utils.md b/docs/utils.md index cf7d5f1d1..a84f068e9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -264,7 +264,7 @@ All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpr ## List Timeframes -Use the `list-timeframes` subcommand to see the list of timeframes (ticker intervals) available for the exchange. +Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. ``` usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..12c03d824 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -118,7 +118,7 @@ AVAILABLE_CLI_OPTIONS = { # Optimize common "timeframe": Arg( '-i', '--timeframe', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).', ), "timerange": Arg( '--timerange', diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 5f53fc824..d509bfaa5 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -99,7 +99,7 @@ def start_list_hyperopts(args: Dict[str, Any]) -> None: def start_list_timeframes(args: Dict[str, Any]) -> None: """ - Print ticker intervals (timeframes) available on Exchange + Print timeframes available on Exchange """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Do not use timeframe set in the config diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..a9bbc021c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -31,7 +31,7 @@ class IHyperOpt(ABC): Defines the mandatory structure must follow any custom hyperopt Class attributes you can use: - ticker_interval -> int: value of the ticker interval to use for the strategy + timeframe -> int: value of the timeframe to use for the strategy """ ticker_interval: str # DEPRECATED timeframe: str @@ -97,7 +97,7 @@ class IHyperOpt(ABC): This method implements adaptive roi hyperspace with varied ranges for parameters which automatically adapts to the - ticker interval used. + timeframe used. It's used by Freqtrade by default, if no custom roi_space method is defined. """ @@ -119,7 +119,7 @@ class IHyperOpt(ABC): # * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically. # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() - # method for the 5m ticker interval. + # method for the 5m timeframe. roi_t_scale = timeframe_min / 5 roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_limits = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 5dfa42bcc..904597d21 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -53,7 +53,7 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. + # Optimal timeframe for the strategy. timeframe = '5m' # Run "populate_indicators()" only for new candle. diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 0ba6f4a7f..3655b941d 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -268,7 +268,7 @@ tc16 = BTContainer(data=[ # Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) -# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval. +# Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe. tc17 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index 98842ff7c..7171b93ae 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -28,7 +28,7 @@ class DefaultStrategy(IStrategy): # Optimal stoploss designed for the strategy stoploss = -0.10 - # Optimal ticker interval for the strategy + # Optimal timeframe for the strategy timeframe = '5m' # Optional order type mapping diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 1e7bb5e1e..9ef00b110 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -31,7 +31,7 @@ class TestStrategyLegacy(IStrategy): # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.10 - # Optimal ticker interval for the strategy + # Optimal timeframe for the strategy # Keep the legacy value here to test compatibility ticker_interval = '5m' From 5f6eae52a296a83b6426bb6c8f0104bc59577c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 19:12:36 +0200 Subject: [PATCH 057/460] fix too long performance message closes #4655 --- freqtrade/rpc/telegram.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92899d67f..b418c3dab 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -695,14 +695,18 @@ class Telegram(RPCHandler): """ try: trades = self._rpc._rpc_performance() - stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( - index=i + 1, - pair=trade['pair'], - profit=trade['profit'], - count=trade['count'] - ) for i, trade in enumerate(trades)) - message = 'Performance:\n{}'.format(stats) - self._send_msg(message, parse_mode=ParseMode.HTML) + output = "Performance:\n" + for i, trade in enumerate(trades): + stat_line = (f"{i+1}.\t {trade['pair']}\t{trade['profit']:.2f}% " + f"({trade['count']})\n") + + if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output) + output = stat_line + else: + output += stat_line + + self._send_msg(output, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 9d4b5cc6bb961125df6a5afa94ed62848537f010 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 17:10:39 +0200 Subject: [PATCH 058/460] Fix typo --- freqtrade/optimize/hyperopt_interface.py | 2 +- freqtrade/templates/sample_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 8eefff99c..633c8bdd5 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -103,7 +103,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_min = timeframe_to_minutes(self.ticker_interval) + timeframe_min = timeframe_to_minutes(self.timeframe) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 29b550ea4..a51b30f3f 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -57,7 +57,7 @@ class SampleStrategy(IStrategy): # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) - sell_rsi = IntParameter(low=50, high=100, defualt=70, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) # Optimal timeframe for the strategy. timeframe = '5m' From 30e5e9296817f54ca773772a124548fd290c9d9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 20:17:48 +0200 Subject: [PATCH 059/460] Don't allow one parmeter to be in 2 spaces use the explicit user wish (given explicitly with "space") --- freqtrade/strategy/hyper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 6282d91c0..e7f31e20d 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -244,8 +244,8 @@ class HyperStrategyMixin(object): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): - if category is None or category == attr.category or \ - attr_name.startswith(category + '_'): + if (category is None or category == attr.category or + (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr def _load_params(self, params: dict) -> None: From 9e56f6d4ebba51b07b6facb81da588306741d488 Mon Sep 17 00:00:00 2001 From: rextea Date: Sun, 4 Apr 2021 01:19:38 +0300 Subject: [PATCH 060/460] Sort pair lists by total profit --- freqtrade/optimize/optimize_reports.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 099976aa9..a80dc5d31 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -110,6 +110,9 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b tabular_data.append(_generate_result_line(result, starting_balance, pair)) + # Sort by total profit %: + tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) + # Append Total tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data From c2be9b971c89795adcbdc98bd2aee2ae8a039c5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Apr 2021 07:02:59 +0200 Subject: [PATCH 061/460] Improve backtest assumptions with fill rules --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index c8acfdbe1..e16225f94 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -420,6 +420,7 @@ 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 +- All orders are filled at the requested price (no slippage, no unfilled orders) - 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 From 342f14472c1e6fb4082218424d865cb1c97675d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:26:45 +0000 Subject: [PATCH 062/460] Bump mkdocs-material from 7.0.7 to 7.1.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.7 to 7.1.0. - [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/7.0.7...7.1.0) 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 711b6ca46..3dbaea111 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.7 +mkdocs-material==7.1.0 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From fc2f9fd0c77cf0791952e42f39cd25e54f042cbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:16 +0000 Subject: [PATCH 063/460] Bump ccxt from 1.45.44 to 1.46.38 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.45.44 to 1.46.38. - [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.45.44...1.46.38) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..5177ae5d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.45.44 +ccxt==1.46.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 320172a224172582a0156cda6ccd7e4f0fad69c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:26 +0000 Subject: [PATCH 064/460] Bump sqlalchemy from 1.4.3 to 1.4.5 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.3 to 1.4.5. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..9b3638789 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.45.44 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.3 +SQLAlchemy==1.4.5 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 36b39f91365400f011410f0ac2830b456040b1bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:31 +0000 Subject: [PATCH 065/460] Bump pycoingecko from 1.4.0 to 1.4.1 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.4.0...1.4.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 e4984cf47..964701830 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.19 technical==1.2.2 tabulate==0.8.9 -pycoingecko==1.4.0 +pycoingecko==1.4.1 jinja2==2.11.3 tables==3.6.1 blosc==1.10.2 From abbc56c1ccae81cd32e013f698f1eb60b601deaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:44 +0000 Subject: [PATCH 066/460] Bump pytest from 6.2.2 to 6.2.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.2...6.2.3) 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 02f7fbca8..cd93f2433 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.9.0 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 -pytest==6.2.2 +pytest==6.2.3 pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 From 0407bf755f157cf812b718faea5ac8338ae9787d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 07:28:51 +0200 Subject: [PATCH 067/460] Use .query.session to make sure the scoped session is used properly --- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/persistence/models.py | 21 +++++++++----------- freqtrade/persistence/pairlock_middleware.py | 6 +++--- freqtrade/rpc/rpc.py | 6 +++--- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dd6966848..a701e8db9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -187,7 +187,7 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() - Trade.session.flush() + Trade.query.session.flush() def process_stopped(self) -> None: """ @@ -621,8 +621,8 @@ class FreqtradeBot(LoggingMixin): if order_status == 'closed': self.update_trade_state(trade, order_id, order) - Trade.session.add(trade) - Trade.session.flush() + Trade.query.session.add(trade) + Trade.query.session.flush() # Updating wallets self.wallets.update() @@ -1205,7 +1205,7 @@ class FreqtradeBot(LoggingMixin): # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) - Trade.session.flush() + Trade.query.session.flush() # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 465f3d443..a82c047c3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -61,11 +61,8 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # We should use the scoped_session object - not a seperately initialized version Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.query = Trade.session.query_property() - # Copy session attributes to order object too - Order.session = Trade.session - Order.query = Order.session.query_property() - PairLock.session = Trade.session - PairLock.query = PairLock.session.query_property() + Order.query = Trade.session.query_property() + PairLock.query = Trade.session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) @@ -81,7 +78,7 @@ def cleanup_db() -> None: Flushes all pending operations to disk. :return: None """ - Trade.session.flush() + Trade.query.session.flush() def clean_dry_run_db() -> None: @@ -677,7 +674,7 @@ class LocalTrade(): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.session.query( + total_open_stake_amount = Trade.query.with_entities( func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() else: total_open_stake_amount = sum( @@ -689,7 +686,7 @@ class LocalTrade(): """ Returns List of dicts containing all Trades, including profit and trade count """ - pair_rates = Trade.session.query( + pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.count(Trade.pair).label('count') @@ -712,7 +709,7 @@ class LocalTrade(): Get best pair with closed trade. :returns: Tuple containing (pair, profit_sum) """ - best_pair = Trade.session.query( + best_pair = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum') ).filter(Trade.is_open.is_(False)) \ .group_by(Trade.pair) \ @@ -805,10 +802,10 @@ class Trade(_DECL_BASE, LocalTrade): def delete(self) -> None: for order in self.orders: - Order.session.delete(order) + Order.query.session.delete(order) - Trade.session.delete(self) - Trade.session.flush() + Trade.query.session.delete(self) + Trade.query.session.flush() @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index f0048bb52..245f7cdab 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -48,8 +48,8 @@ class PairLocks(): active=True ) if PairLocks.use_db: - PairLock.session.add(lock) - PairLock.session.flush() + PairLock.query.session.add(lock) + PairLock.query.session.flush() else: PairLocks.locks.append(lock) @@ -99,7 +99,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock.session.flush() + PairLock.query.session.flush() @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1359729b9..59758a573 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -558,7 +558,7 @@ class RPC: # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) - Trade.session.flush() + Trade.query.session.flush() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} @@ -571,7 +571,7 @@ class RPC: raise RPCException('invalid argument') _exec_forcesell(trade) - Trade.session.flush() + Trade.query.session.flush() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} @@ -696,7 +696,7 @@ class RPC: lock.lock_end_time = datetime.now(timezone.utc) # session is always the same - PairLock.session.flush() + PairLock.query.session.flush() return self._rpc_locks() From ea0b47a7f9c1625e6ec651d656688e19bff41781 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 07:38:07 +0200 Subject: [PATCH 068/460] Replace test occurances of Trade.session with Trade.query.session --- tests/conftest.py | 2 +- tests/plugins/test_protections.py | 42 +++++++++++++++---------------- tests/rpc/test_rpc_apiserver.py | 12 ++++----- tests/test_freqtradebot.py | 35 +++++++++++--------------- tests/test_integration.py | 1 - tests/test_persistence.py | 8 +++--- 6 files changed, 46 insertions(+), 54 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3522ef02d..f8c1c5357 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -197,7 +197,7 @@ def create_mock_trades(fee, use_db: bool = True): """ def add_trade(trade): if use_db: - Trade.session.add(trade) + Trade.query.session.add(trade) else: LocalTrade.add_bt_trade(trade) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2e42c1be4..545387eaa 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -91,7 +91,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -100,12 +100,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): 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( + Trade.query.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( + Trade.query.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, )) @@ -114,7 +114,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -148,7 +148,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -158,12 +158,12 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair 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( + Trade.query.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( + Trade.query.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, )) @@ -178,7 +178,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair caplog.clear() # 2nd Trade that counts with correct pair - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -203,7 +203,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -213,7 +213,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=205, min_ago_close=35, )) @@ -242,7 +242,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.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, profit_rate=0.9, )) @@ -253,7 +253,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.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, profit_rate=0.9, )) @@ -265,14 +265,14 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() # Add positive trade - Trade.session.add(generate_mock_trade( + Trade.query.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( + Trade.query.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, profit_rate=0.8, )) @@ -300,15 +300,15 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.stop_per_pair('XRP/BTC') caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.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( + Trade.query.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( + Trade.query.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, )) @@ -316,7 +316,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -326,7 +326,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -339,7 +339,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) # Winning trade ... (should not lock, does not change drawdown!) - Trade.session.add(generate_mock_trade( + Trade.query.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, )) @@ -349,7 +349,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): caplog.clear() # Add additional negative trade, causing a loss of > 15% - Trade.session.add(generate_mock_trade( + Trade.query.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, )) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 20d32024f..180eefa08 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -510,7 +510,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 0 create_mock_trades(fee) - Trade.session.flush() + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) @@ -538,7 +538,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): assert_response(rc, 502) create_mock_trades(fee) - Trade.session.flush() + Trade.query.session.flush() ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() trades[1].stoploss_order_id = '1234' @@ -720,7 +720,7 @@ def test_api_performance(botclient, mocker, ticker, fee): ) trade.close_profit = trade.calc_profit_ratio() - Trade.session.add(trade) + Trade.query.session.add(trade) trade = Trade( pair='XRP/ETH', @@ -735,8 +735,8 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.391 ) trade.close_profit = trade.calc_profit_ratio() - Trade.session.add(trade) - Trade.session.flush() + Trade.query.session.add(trade) + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) @@ -764,7 +764,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): trades = Trade.get_open_trades() trades[0].open_order_id = None ftbot.exit_positions(trades) - Trade.session.flush() + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 486c31090..c93f8b858 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -768,7 +768,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, assert pair not in default_conf['exchange']['pair_whitelist'] # create open trade not in whitelist - Trade.session.add(Trade( + Trade.query.session.add(Trade( pair=pair, stake_amount=0.001, fee_open=fee.return_value, @@ -778,7 +778,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, open_rate=0.01, exchange='bittrex', )) - Trade.session.add(Trade( + Trade.query.session.add(Trade( pair='ETH/BTC', stake_amount=0.001, fee_open=fee.return_value, @@ -1779,7 +1779,6 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) - Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf) trade = Trade( @@ -1805,7 +1804,6 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) - Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf) trade = Trade( @@ -1868,7 +1866,6 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) patch_exchange(mocker) - Trade.session = MagicMock() amount = limit_sell_order["amount"] freqtrade = get_patched_freqtradebot(mocker, default_conf) wallet_mock.reset_mock() @@ -2110,7 +2107,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # Ensure default is to return empty (so not mocked yet) freqtrade.check_handle_timedout() @@ -2161,7 +2158,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) # check it does cancel buy orders over the time limit @@ -2191,7 +2188,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() @@ -2218,7 +2215,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() @@ -2248,7 +2245,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ open_trade.close_profit_abs = 0.001 open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # Ensure default is false freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 @@ -2296,7 +2293,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, open_trade.close_profit_abs = 0.001 open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) # check it does cancel sell orders over the time limit @@ -2327,7 +2324,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() @@ -2353,7 +2350,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit # note this is for a partially-complete buy order @@ -2386,7 +2383,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. freqtrade.check_handle_timedout() @@ -2426,7 +2423,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. freqtrade.check_handle_timedout() @@ -2463,7 +2460,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.check_handle_timedout() assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, " @@ -2486,7 +2483,6 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non freqtrade = FreqtradeBot(default_conf) freqtrade._notify_buy_cancel = MagicMock() - Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['filled'] = 0.0 @@ -2520,7 +2516,6 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel') freqtrade = FreqtradeBot(default_conf) - Trade.session = MagicMock() reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' @@ -2549,7 +2544,6 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) freqtrade._notify_buy_cancel = MagicMock() - Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['filled'] = 0.0 @@ -2812,7 +2806,6 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c freqtrade.enter_positions() trade = Trade.query.first() - Trade.session = MagicMock() PairLock.session = MagicMock() freqtrade.config['dry_run'] = False @@ -4422,7 +4415,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): open_rate=0.01, exchange='bittrex', ) - Trade.session.add(trade) + Trade.query.session.add(trade) freqtrade.reupdate_buy_order_fees(trade) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index 8e3bd251a..1c60faa7b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -89,7 +89,6 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, freqtrade.strategy.confirm_trade_entry.reset_mock() assert freqtrade.strategy.confirm_trade_exit.call_count == 0 wallets_mock.reset_mock() - Trade.session = MagicMock() trades = Trade.query.all() # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6a388327c..d53386287 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -403,7 +403,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='dry_run_buy_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) trade = Trade( pair='ETC/BTC', @@ -415,7 +415,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='dry_run_sell_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) # Simulate prod entry trade = Trade( @@ -428,7 +428,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='prod_buy_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) # We have 3 entries: 2 dry_run, 1 prod assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3 @@ -933,7 +933,7 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 - Trade.session.add(trade) + Trade.query.session.add(trade) # Lower stoploss Trade.stoploss_reinitialization(0.06) From e979f132e3933effcfd2e2fb0d742b1c559883a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:57:56 +0000 Subject: [PATCH 069/460] Bump python from 3.9.2-slim-buster to 3.9.3-slim-buster Bumps python from 3.9.2-slim-buster to 3.9.3-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b399174b..1f6e36b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.2-slim-buster as base +FROM python:3.9.3-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index eecd9fdc0..5e6763d4c 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.3-slim-buster as base # Setup env ENV LANG C.UTF-8 From af525818136efd4848a5b6e6e6d6c70653310d53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 08:22:01 +0200 Subject: [PATCH 070/460] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5e6763d4c..dcb008cb9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.3-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From 7132aefd608ab8495932f438bc66695d18d4d70f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 08:46:12 +0200 Subject: [PATCH 071/460] Rename Trade.session to Trade._session --- freqtrade/persistence/models.py | 8 ++++---- tests/test_persistence.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a82c047c3..a22e75e1e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -59,10 +59,10 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) - Trade.query = Trade.session.query_property() - Order.query = Trade.session.query_property() - PairLock.query = Trade.session.query_property() + Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) + Trade.query = Trade._session.query_property() + Order.query = Trade._session.query_property() + PairLock.query = Trade._session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d53386287..3336e4e66 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -18,8 +18,8 @@ from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url'], default_conf['dry_run']) - assert hasattr(Trade, 'session') - assert 'scoped_session' in type(Trade.session).__name__ + assert hasattr(Trade, '_session') + assert 'scoped_session' in type(Trade._session).__name__ def test_init_custom_db_url(default_conf, tmpdir): From dc406fe19f85197d9223342c7777a466caabb480 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 10:53:00 +0200 Subject: [PATCH 072/460] Fail in case of name and explicit space name collisions --- freqtrade/strategy/hyper.py | 4 ++++ tests/optimize/test_hyperopt.py | 2 -- tests/strategy/test_interface.py | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e7f31e20d..0b7055f01 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -244,6 +244,10 @@ class HyperStrategyMixin(object): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): + if (category and attr_name.startswith(category + '_') + and attr.category is not None and attr.category != category): + raise OperationalException( + f'Inconclusive parameter name {attr_name}, category: {attr.category}.') if (category is None or category == attr.category or (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 36b6f1229..c13da0d76 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1093,8 +1093,6 @@ def test_print_epoch_details(capsys): def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: - # mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - # mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed del hyperopt_conf['hyperopt'] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 71f877cc3..3bfa691b4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -617,3 +617,8 @@ def test_auto_hyperopt_interface(default_conf): # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 + + strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') + + with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): + [x for x in strategy.enumerate_parameters('sell')] From c51839dc3be4c47a32394719eb3bfba0e94ff1a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 11:21:20 +0200 Subject: [PATCH 073/460] Make the logmessage for loaded parameters clearer --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0b7055f01..709179997 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -263,7 +263,7 @@ class HyperStrategyMixin(object): if attr_name in params: if attr.load: attr.value = params[attr_name] - logger.info(f'{attr_name} = {attr.value}') + logger.info(f'Strategy Parameter: {attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') From 3044aa18e60bb5d42d880d67b27a7bea0f937653 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 14:45:42 +0200 Subject: [PATCH 074/460] Add warning for hyperopt-parameters --- docs/hyperopt.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index db7a23f02..07cc963cf 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -305,6 +305,9 @@ There are four parameter types each suited for different purposes. * `optimize` - when set to `False` parameter will not be included in optimization process. Use these parameters to quickly prototype various ideas. +!!! Warning + Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. From 7b2a0d46cb19cb4c219d33e4ec1fc9cfac887a40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 15:38:25 +0200 Subject: [PATCH 075/460] Fix typo --- freqtrade/optimize/hyperopt_auto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index ed6f2d6f7..c4d6f1581 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -33,14 +33,14 @@ class HyperOptAuto(IHyperOpt): return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - def populate_buy_trend(dataframe: DataFrame, metadata: dict): + def populate_sell_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) - return populate_buy_trend + return populate_sell_trend def _get_func(self, name) -> Callable: """ From 78a84f8081f8cd58f0ab7a74f2f40c9106dd615a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 15:38:33 +0200 Subject: [PATCH 076/460] Allow --hyperoptloss in addition to --hyperopt-loss --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index cea353109..4fac8ac72 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -267,7 +267,7 @@ AVAILABLE_CLI_OPTIONS = { default=1, ), "hyperopt_loss": Arg( - '--hyperopt-loss', + '--hyperopt-loss', '--hyperoptloss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' From c176e277f117badcbe495750707af79e07717b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:31:34 +0200 Subject: [PATCH 077/460] Add a REST endpoint for getting a specific trade --- freqtrade/rpc/api_server/api_v1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..6873c0c4c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -88,6 +88,11 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) +@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) +def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_status([tradeid])[0] + + @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete(tradeid) From ddba0d688e0b168054fda58b0267810ff13fad98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:32:55 +0200 Subject: [PATCH 078/460] Add new trade endpoint to docs --- docs/rest-api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index c41c3f24c..4e784b6af 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -125,6 +125,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. | `trades` | List last trades. +| `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. | `logs` | Shows last log messages. @@ -275,6 +276,10 @@ trades :param limit: Limits trades to the X last trades. No limit to get all the trades. +trade + Return specific trade. + :param tradeid: Specify which trade to get. + version Return the version of the bot. From fc78246bbc2a7f4260e1bf2bdd3d898c52c98110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:34:01 +0200 Subject: [PATCH 079/460] Some changes to rest-api docs --- docs/rest-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 4e784b6af..be3107fcb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -182,7 +182,7 @@ count Return the amount of open trades. daily - Return the amount of open trades. + Return the profits for each day, and amount of trades. delete_lock Delete (disable) lock from the database. @@ -215,7 +215,7 @@ locks logs Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. pair_candles Return live dataframe for . From 6633752fcb743f693105c9c56633c82817593592 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 05:29:34 +0000 Subject: [PATCH 080/460] Bump python from 3.9.3-slim-buster to 3.9.4-slim-buster Bumps python from 3.9.3-slim-buster to 3.9.4-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1f6e36b68..711f27990 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.3-slim-buster as base +FROM python:3.9.4-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index dcb008cb9..b019b6096 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.4-slim-buster as base # Setup env ENV LANG C.UTF-8 From 0550f261f1f48626c112ef830485ac5a843f9548 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:47:44 +0200 Subject: [PATCH 081/460] Add exchange_has validation --- freqtrade/commands/list_commands.py | 14 +++++++++----- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/common.py | 23 +++++++++++++++++++++++ freqtrade/exchange/exchange.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index d509bfaa5..fa4bc1066 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -13,7 +13,7 @@ from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException -from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active +from freqtrade.exchange import market_is_active, validate_exchanges from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -28,14 +28,18 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: :param args: Cli args from Arguments() :return: None """ - exchanges = ccxt_exchanges() if args['list_exchanges_all'] else available_exchanges() + exchanges = validate_exchanges(args['list_exchanges_all']) + if args['print_one_column']: - print('\n'.join(exchanges)) + print('\n'.join([e[0] for e in exchanges])) else: if args['list_exchanges_all']: - print(f"All exchanges supported by the ccxt library: {', '.join(exchanges)}") + print("All exchanges supported by the ccxt library:") else: - print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}") + print("Exchanges available for Freqtrade:") + exchanges = [e for e in exchanges if e[1] is not False] + + print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason'])) def _print_objs_tabular(objs: List, print_colorized: bool) -> None: diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 15ba7b9f6..0eedd25d8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,6 +12,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds) + timeframe_to_seconds, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index be0a1e483..90b70d67e 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -98,6 +98,29 @@ MAP_EXCHANGE_CHILDCLASS = { } +EXCHANGE_HAS_REQUIRED = [ + # Required / private + 'fetchOrder', + 'cancelOrder', + 'createOrder', + # 'createLimitOrder', 'createMarketOrder', + 'fetchBalance', + + # Public endpoints + 'loadMarkets', + 'fetchOHLCV', +] + +EXCHANGE_HAS_OPTIONAL = [ + # Private + 'fetchMyTrades', # Trades for order - fee detection + # Public + 'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker', # OR for pricing + 'fetchTickers', # For volumepairlist? + 'fetchTrades', # Downloading trades data +] + + def calculate_backoff(retrycount, max_retries): """ Calculate backoff diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 85c5b4c6d..768a02aa1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,7 +23,8 @@ from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, + EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -1337,6 +1338,32 @@ def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: return [x for x in exchanges if not is_exchange_bad(x)] +def validate_exchange(exchange: str) -> Tuple[bool, str]: + ex_mod = getattr(ccxt, exchange.lower())() + if not ex_mod or not ex_mod.has: + return False, '' + missing = [k for k in EXCHANGE_HAS_REQUIRED if not ex_mod.has.get(k)] + if missing: + return False, f"missing: {', '.join(missing)}" + + missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + if missing_opt: + return True, f"missing opt: {', '.join(missing_opt)}" + + return True, '' + + +def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]: + """ + :return: List of tuples with exchangename, valid, reason. + """ + exchanges = ccxt_exchanges() if all_exchanges else available_exchanges() + exchanges_valid = [ + (e, *validate_exchange(e)) for e in exchanges + ] + return exchanges_valid + + def timeframe_to_seconds(timeframe: str) -> int: """ Translates the timeframe interval value written in the human readable From 969d44a95298e20174514017c85a9925245699ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:49:16 +0200 Subject: [PATCH 082/460] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b019b6096..dcb008cb9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.4-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From ddabfe0206c4fa93efb04942dc4c11c6a062d026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:57:27 +0200 Subject: [PATCH 083/460] adjust tests to match new exchangelist output --- tests/commands/test_commands.py | 10 +++++----- tests/test_main.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index e21ef4dd1..232fc4e2c 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -66,8 +66,8 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() assert re.match(r"Exchanges available for Freqtrade.*", captured.out) - assert re.match(r".*binance,.*", captured.out) - assert re.match(r".*bittrex,.*", captured.out) + assert re.search(r".*binance.*", captured.out) + assert re.search(r".*bittrex.*", captured.out) # Test with --one-column args = [ @@ -89,9 +89,9 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() assert re.match(r"All exchanges supported by the ccxt library.*", captured.out) - assert re.match(r".*binance,.*", captured.out) - assert re.match(r".*bittrex,.*", captured.out) - assert re.match(r".*bitmex,.*", captured.out) + assert re.search(r".*binance.*", captured.out) + assert re.search(r".*bittrex.*", captured.out) + assert re.search(r".*bitmex.*", captured.out) # Test with --one-column --all args = [ diff --git a/tests/test_main.py b/tests/test_main.py index 70632aeaa..d52dcaf79 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -118,7 +118,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_operational_exception1(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch( - 'freqtrade.commands.list_commands.available_exchanges', + 'freqtrade.commands.list_commands.validate_exchanges', MagicMock(side_effect=ValueError('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) @@ -132,7 +132,7 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has('Fatal exception!', caplog) assert not log_has_re(r'SIGINT.*', caplog) mocker.patch( - 'freqtrade.commands.list_commands.available_exchanges', + 'freqtrade.commands.list_commands.validate_exchanges', MagicMock(side_effect=KeyboardInterrupt) ) with pytest.raises(SystemExit): From 142690c93068be58937aaef08dd9004871f237fb Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:05:03 +0200 Subject: [PATCH 084/460] resolves freqtrade/freqtrade#4650 --- config_binance.json.example | 99 --------------- docker-compose.yml | 2 +- freqtrade/constants.py | 4 +- .../plugins/pairlist/VolatilityFilter.py | 120 ++++++++++++++++++ 4 files changed, 123 insertions(+), 102 deletions(-) delete mode 100644 config_binance.json.example create mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/config_binance.json.example b/config_binance.json.example deleted file mode 100644 index 4fa615d6d..000000000 --- a/config_binance.json.example +++ /dev/null @@ -1,99 +0,0 @@ -{ - "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.05, - "tradable_balance_ratio": 0.99, - "fiat_display_currency": "USD", - "timeframe": "5m", - "dry_run": true, - "cancel_open_orders_on_exit": false, - "unfilledtimeout": { - "buy": 10, - "sell": 30 - }, - "bid_strategy": { - "ask_last_balance": 0.0, - "use_order_book": false, - "order_book_top": 1, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "ask_strategy": { - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, - "use_sell_signal": true, - "sell_profit_only": false, - "ignore_roi_if_buy_signal": false - }, - "exchange": { - "name": "binance", - "key": "your_exchange_key", - "secret": "your_exchange_secret", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, - "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" - ], - "pair_blacklist": [ - "BNB/BTC" - ] - }, - "pairlists": [ - {"method": "StaticPairList"} - ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, - "telegram": { - "enabled": false, - "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id" - }, - "api_server": { - "enabled": false, - "listen_ip_address": "127.0.0.1", - "listen_port": 8080, - "verbosity": "error", - "jwt_secret_key": "somethingrandom", - "CORS_origins": [], - "username": "freqtrader", - "password": "SuperSecurePassword" - }, - "bot_name": "freqtrade", - "initial_state": "running", - "forcebuy_enable": false, - "internals": { - "process_throttle_secs": 5 - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 80e194ab2..71572140c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --strategy SampleStrategy + --strategy BinHV45.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..c4a360d18 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter'] + 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 @@ -416,4 +416,4 @@ PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list -TradeList = List[List] +TradeList = List[List] \ No newline at end of file diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py new file mode 100644 index 000000000..ea1ebeb29 --- /dev/null +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -0,0 +1,120 @@ +""" +Rate of change pairlist filter +""" +import logging +from copy import deepcopy +from typing import Any, Dict, List, Optional + +import sys +import arrow +from cachetools.ttl import TTLCache +from pandas import DataFrame +import numpy as np + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.plugins.pairlist.IPairList import IPairList + + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + ''' + Filters pairs by volatility + ''' + + 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('lookback_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0) + self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit('1d'): + raise OperationalException("VolatilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit('1d')})") + + @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 + """ + return (f"{self.name} - Filtering pairs with volatility range " + f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + result = False + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_avg = volatility_series.mean() + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result + + return result From 6f02acdbbd8d1f524e552ae9e24a775fbe7071b1 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:39:27 +0200 Subject: [PATCH 085/460] Revert "resolves freqtrade/freqtrade#4650" This reverts commit 142690c93068be58937aaef08dd9004871f237fb. --- config_binance.json.example | 99 +++++++++++++++ docker-compose.yml | 2 +- freqtrade/constants.py | 4 +- .../plugins/pairlist/VolatilityFilter.py | 120 ------------------ 4 files changed, 102 insertions(+), 123 deletions(-) create mode 100644 config_binance.json.example delete mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/config_binance.json.example b/config_binance.json.example new file mode 100644 index 000000000..4fa615d6d --- /dev/null +++ b/config_binance.json.example @@ -0,0 +1,99 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 1, + "use_sell_signal": true, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 200 + }, + "pair_whitelist": [ + "ALGO/BTC", + "ATOM/BTC", + "BAT/BTC", + "BCH/BTC", + "BRD/BTC", + "EOS/BTC", + "ETH/BTC", + "IOTA/BTC", + "LINK/BTC", + "LTC/BTC", + "NEO/BTC", + "NXS/BTC", + "XMR/BTC", + "XRP/BTC", + "XTZ/BTC" + ], + "pair_blacklist": [ + "BNB/BTC" + ] + }, + "pairlists": [ + {"method": "StaticPairList"} + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "jwt_secret_key": "somethingrandom", + "CORS_origins": [], + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 71572140c..80e194ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --strategy BinHV45.py + --strategy SampleStrategy diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c4a360d18..3a2ed98e9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter', 'VolatilityFilter'] + 'SpreadFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 @@ -416,4 +416,4 @@ PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list -TradeList = List[List] \ No newline at end of file +TradeList = List[List] diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py deleted file mode 100644 index ea1ebeb29..000000000 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Rate of change pairlist filter -""" -import logging -from copy import deepcopy -from typing import Any, Dict, List, Optional - -import sys -import arrow -from cachetools.ttl import TTLCache -from pandas import DataFrame -import numpy as np - -from freqtrade.exceptions import OperationalException -from freqtrade.misc import plural -from freqtrade.plugins.pairlist.IPairList import IPairList - - - -logger = logging.getLogger(__name__) - - -class VolatilityFilter(IPairList): - ''' - Filters pairs by volatility - ''' - - 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('lookback_days', 10) - self._min_volatility = pairlistconfig.get('min_volatility', 0) - self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) - self._refresh_period = pairlistconfig.get('refresh_period', 1440) - - self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) - - if self._days < 1: - raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") - if self._days > exchange.ohlcv_candle_limit('1d'): - raise OperationalException("VolatilityFilter requires lookback_days to not " - "exceed exchange max request size " - f"({exchange.ohlcv_candle_limit('1d')})") - - @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 - """ - return (f"{self.name} - Filtering pairs with volatility range " - f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") - - def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: - """ - Validate trading range - :param pairlist: pairlist to filter or sort - :param tickers: Tickers (from exchange.get_tickers()). May be cached. - :return: new allowlist - """ - needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] - - since_ms = int(arrow.utcnow() - .floor('day') - .shift(days=-self._days - 1) - .float_timestamp) * 1000 - # Get all candles - candles = {} - if needed_pairs: - candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, - cache=False) - - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist - - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: - """ - Validate trading range - :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, false if it should be removed - """ - # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] - - result = False - if daily_candles is not None and not daily_candles.empty: - returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) - returns.fillna(0, inplace=True) - - volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) - volatility_avg = volatility_series.mean() - - if self._min_volatility <= volatility_avg <= self._max_volatility: - result = True - else: - self.log_once(f"Removed {pair} from whitelist, because volatility " - f"over {self._days} {plural(self._days, 'day')} " - f"is: {volatility_avg:.3f} " - f"which is not in the configured range of " - f"{self._min_volatility}-{self._max_volatility}.", - logger.info) - result = False - self._pair_cache[pair] = result - - return result From be770a89417b90a82d537ac5f26d2d76ee726a76 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:42:53 +0200 Subject: [PATCH 086/460] added VolatilityFilter resolves freqtrade#4650 --- freqtrade/constants.py | 2 +- .../plugins/pairlist/VolatilityFilter.py | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..b98161ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter'] + 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py new file mode 100644 index 000000000..97e86bab6 --- /dev/null +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -0,0 +1,120 @@ +""" +Rate of change pairlist filter +""" +import logging +from copy import deepcopy +from typing import Any, Dict, List, Optional + +import sys +import arrow +from cachetools.ttl import TTLCache +from pandas import DataFrame +import numpy as np + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.plugins.pairlist.IPairList import IPairList + + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + ''' + Filters pairs by volatility + ''' + + 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('lookback_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0) + self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit('1d'): + raise OperationalException("VolatilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit('1d')})") + + @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 + """ + return (f"{self.name} - Filtering pairs with volatility range " + f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + result = False + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_avg = volatility_series.mean() + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result + + return result \ No newline at end of file From 1733e24062339363084200e12f1bbda26f41dde0 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:44:13 +0200 Subject: [PATCH 087/460] pyLint adjustment resolves freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 97e86bab6..1913bfcc1 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -117,4 +117,5 @@ class VolatilityFilter(IPairList): result = False self._pair_cache[pair] = result - return result \ No newline at end of file + return result + \ No newline at end of file From 56ef3af42415164400a3f24bd1e9b2754e467491 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 11:59:58 +0200 Subject: [PATCH 088/460] Allow comments in pairs files --- freqtrade/configuration/configuration.py | 12 +++++------- freqtrade/configuration/load_config.py | 9 +++++++++ freqtrade/misc.py | 2 +- tests/test_configuration.py | 19 ++++++------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index a40a4fd83..9acd532cc 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -11,10 +11,10 @@ from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir -from freqtrade.configuration.load_config import load_config_file +from freqtrade.configuration.load_config import load_config_file, load_file from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts, json_load +from freqtrade.misc import deep_merge_dicts from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode @@ -454,9 +454,8 @@ class Configuration: # or if pairs file is specified explicitely if not pairs_file.exists(): raise OperationalException(f'No pairs file found with path "{pairs_file}".') - with pairs_file.open('r') as f: - config['pairs'] = json_load(f) - config['pairs'].sort() + config['pairs'] = load_file(pairs_file) + config['pairs'].sort() return if 'config' in self.args and self.args['config']: @@ -466,7 +465,6 @@ class Configuration: # Fall back to /dl_path/pairs.json pairs_file = config['datadir'] / 'pairs.json' if pairs_file.exists(): - with pairs_file.open('r') as f: - config['pairs'] = json_load(f) + config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 726126034..1320a375f 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -38,6 +38,15 @@ def log_config_error_range(path: str, errmsg: str) -> str: return '' +def load_file(path: Path) -> Dict[str, Any]: + try: + with path.open('r') as file: + config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) + except FileNotFoundError: + raise OperationalException(f'File file "{path}" not found!') + return config + + def load_config_file(path: str) -> Dict[str, Any]: """ Loads a config file from the given path diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7bbc24056..6508363d6 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,7 +81,7 @@ def json_load(datafile: IO) -> Any: """ load data with rapidjson Use this to have a consistent experience, - sete number_mode to "NM_NATIVE" for greatest speed + set number_mode to "NM_NATIVE" for greatest speed """ return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 15fbab7f8..b8d38dce0 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1038,37 +1038,30 @@ def test_pairlist_resolving_with_config(mocker, default_conf): def test_pairlist_resolving_with_config_pl(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) - load_mock = mocker.patch("freqtrade.configuration.configuration.json_load", - MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) arglist = [ 'download-data', '--config', 'config.json', - '--pairs-file', 'pairs.json', + '--pairs-file', 'tests/testdata/pairs.json', ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() - - assert load_mock.call_count == 1 - assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert len(config['pairs']) == 23 + assert 'ETH/BTC' in config['pairs'] + assert 'XRP/BTC' in config['pairs'] assert config['exchange']['name'] == default_conf['exchange']['name'] def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) - mocker.patch("freqtrade.configuration.configuration.json_load", - MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) arglist = [ 'download-data', '--config', 'config.json', - '--pairs-file', 'pairs.json', + '--pairs-file', 'tests/testdata/pairs_doesnotexist.json', ] args = Arguments(arglist).get_parsed_arg() @@ -1081,7 +1074,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): def test_pairlist_resolving_fallback(mocker): mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) - mocker.patch("freqtrade.configuration.configuration.json_load", + mocker.patch("freqtrade.configuration.configuration.load_file", MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) arglist = [ 'download-data', From bf0886a839eeca7477ade9eeb5b2fa4bc6a28851 Mon Sep 17 00:00:00 2001 From: klara31 Date: Tue, 6 Apr 2021 18:35:30 +0200 Subject: [PATCH 089/460] Update constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..a3cf71553 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -176,7 +176,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, - 'sell_profit_offset': {'type': 'number', 'minimum': 0.0}, + 'sell_profit_offset': {'type': 'number', 'minimum': -100}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, From c40b811f19414042503514ff9fdb97dc062c88cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 19:35:17 +0200 Subject: [PATCH 090/460] flush after creating mock trades --- tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index f8c1c5357..4a2106a4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -220,6 +220,9 @@ def create_mock_trades(fee, use_db: bool = True): trade = mock_trade_6(fee) add_trade(trade) + if use_db: + Trade.query.session.flush() + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: From f37fbbf4e115c3373ad6ef0df660ace3c3c7b83f Mon Sep 17 00:00:00 2001 From: klara31 Date: Tue, 6 Apr 2021 19:47:48 +0200 Subject: [PATCH 091/460] Update constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a3cf71553..2d2f9658c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -176,7 +176,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, - 'sell_profit_offset': {'type': 'number', 'minimum': -100}, + 'sell_profit_offset': {'type': 'number'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, From 5ed7828446abcddc7adb93d81886f66ec8877c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 20:03:38 +0200 Subject: [PATCH 092/460] Remove hardcoded list of non-working exchanges --- freqtrade/exchange/common.py | 70 ------------------------------------ 1 file changed, 70 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 90b70d67e..694aa3aa2 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -18,78 +18,8 @@ BAD_EXCHANGES = { "bitmex": "Various reasons.", "bitstamp": "Does not provide history. " "Details in https://github.com/freqtrade/freqtrade/issues/1983", - "hitbtc": "This API cannot be used with Freqtrade. " - "Use `hitbtc2` exchange id to access this exchange.", "phemex": "Does not provide history. ", "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", - **dict.fromkeys([ - 'adara', - 'anxpro', - 'bigone', - 'coinbase', - 'coinexchange', - 'coinmarketcap', - 'lykke', - 'xbtce', - ], "Does not provide timeframes. ccxt fetchOHLCV: False"), - **dict.fromkeys([ - 'bcex', - 'bit2c', - 'bitbay', - 'bitflyer', - 'bitforex', - 'bithumb', - 'bitso', - 'bitstamp1', - 'bl3p', - 'braziliex', - 'btcbox', - 'btcchina', - 'btctradeim', - 'btctradeua', - 'bxinth', - 'chilebit', - 'coincheck', - 'coinegg', - 'coinfalcon', - 'coinfloor', - 'coingi', - 'coinmate', - 'coinone', - 'coinspot', - 'coolcoin', - 'crypton', - 'deribit', - 'exmo', - 'exx', - 'flowbtc', - 'foxbit', - 'fybse', - # 'hitbtc', - 'ice3x', - 'independentreserve', - 'indodax', - 'itbit', - 'lakebtc', - 'latoken', - 'liquid', - 'livecoin', - 'luno', - 'mixcoins', - 'negociecoins', - 'nova', - 'paymium', - 'southxchange', - 'stronghold', - 'surbitcoin', - 'therock', - 'tidex', - 'vaultoro', - 'vbtc', - 'virwox', - 'yobit', - 'zaif', - ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), } MAP_EXCHANGE_CHILDCLASS = { From b6599c1da9a75e4ac7dae47bd4d7844ea354d0a6 Mon Sep 17 00:00:00 2001 From: Aleksey Popov Date: Tue, 6 Apr 2021 20:10:52 +0200 Subject: [PATCH 093/460] Improve Kraken-specific config description. Added Warning after Kraken rate limit config in order to clearly highlight that it holds delay between requests instead of req\sec rate. --- docs/exchanges.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 4c7e44b06..1c5956088 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -44,6 +44,10 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine. It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient. +!!! Warning "rateLimit tuning" + Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate. + So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased. + ## Bittrex ### Order types From a3b4667f7c39b6b1cb137a245427441469a3e751 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 20:16:29 +0200 Subject: [PATCH 094/460] Update exchange validation to use "validate_exchange". --- freqtrade/configuration/check_exchange.py | 14 +++++++++----- freqtrade/exchange/__init__.py | 4 ++-- freqtrade/exchange/exchange.py | 15 +++++---------- tests/test_configuration.py | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index aa36de3ff..832caf153 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -2,8 +2,8 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad, - is_exchange_known_ccxt, is_exchange_officially_supported) +from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt, + is_exchange_officially_supported, validate_exchange) from freqtrade.state import RunMode @@ -57,9 +57,13 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: f'{", ".join(available_exchanges())}' ) - if check_for_bad and is_exchange_bad(exchange): - raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. ' - f'Reason: {get_exchange_bad_reason(exchange)}') + valid, reason = validate_exchange(exchange) + if not valid: + if check_for_bad: + raise OperationalException(f'Exchange "{exchange}" will not work with Freqtrade. ' + f'Reason: {reason}') + else: + logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') if is_exchange_officially_supported(exchange): logger.info(f'Exchange "{exchange}" is officially supported ' diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 0eedd25d8..8a5563623 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -8,10 +8,10 @@ from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, - get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, validate_exchanges) + timeframe_to_seconds, validate_exchange, + validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 768a02aa1..37d92e253 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1307,14 +1307,6 @@ class Exchange: self.calculate_fee_rate(order)) -def is_exchange_bad(exchange_name: str) -> bool: - return exchange_name in BAD_EXCHANGES - - -def get_exchange_bad_reason(exchange_name: str) -> str: - return BAD_EXCHANGES.get(exchange_name, "") - - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) @@ -1335,18 +1327,21 @@ def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list """ exchanges = ccxt_exchanges(ccxt_module) - return [x for x in exchanges if not is_exchange_bad(x)] + return [x for x in exchanges if validate_exchange(x)[0]] def validate_exchange(exchange: str) -> Tuple[bool, str]: ex_mod = getattr(ccxt, exchange.lower())() if not ex_mod or not ex_mod.has: return False, '' - missing = [k for k in EXCHANGE_HAS_REQUIRED if not ex_mod.has.get(k)] + missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True] if missing: return False, f"missing: {', '.join(missing)}" missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + + if exchange.lower() in BAD_EXCHANGES: + return False, BAD_EXCHANGES.get(exchange.lower(), '') if missing_opt: return True, f"missing opt: {', '.join(missing_opt)}" diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 15fbab7f8..e480b7bc3 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -565,7 +565,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) with pytest.raises(OperationalException, - match=r"Exchange .* is known to not work with the bot yet.*"): + match=r"Exchange .* will not work with Freqtrade\..*"): check_exchange(default_conf) caplog.clear() From 187cf6dcd5217914cea52c3296afb9f58728d017 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 22:41:15 +0200 Subject: [PATCH 095/460] VolatilityFilter resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 6 +++--- tests/plugins/test_pairlist.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 1913bfcc1..1bb836e76 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -67,7 +67,7 @@ class VolatilityFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ - needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] since_ms = int(arrow.utcnow() .floor('day') @@ -81,7 +81,7 @@ class VolatilityFilter(IPairList): if self._enabled: for p in deepcopy(pairlist): - daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) return pairlist @@ -102,7 +102,7 @@ class VolatilityFilter(IPairList): returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) returns.fillna(0, inplace=True) - volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_series = returns.rolling(window=self._days).std()*np.sqrt(self._days) volatility_avg = volatility_series.mean() if self._min_volatility <= volatility_avg <= self._max_volatility: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 67cd96f5b..7d39014f1 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -407,6 +407,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), + ([{"method": "StaticPairList"}, + {"method": "VolatilityFilter", "lookback_days": 3, + "min_volatility": 0.002, "max_volatility": 0.004, "refresh_period": 1440}], + "BTC", ['ETH/BTC', 'TKN/BTC']) ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history, pairlists, base_currency, @@ -414,13 +418,19 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency + ohlcv_history_high_vola = ohlcv_history.copy() + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index==1, 'close'] = 0.00090 + ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, ('LTC/BTC', '1d'): ohlcv_history, ('XRP/BTC', '1d'): ohlcv_history, - ('HOT/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history_high_vola, } + + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -487,7 +497,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert log_has(logmsg, caplog) else: assert not log_has(logmsg, caplog) - + if pairlist["method"] == 'VolatilityFilter': + assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] From 9089323d266980c1d896c35341e3fd3e68dd8362 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 22:46:36 +0200 Subject: [PATCH 096/460] resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 1bb836e76..5b50e04e4 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -1,5 +1,5 @@ """ -Rate of change pairlist filter +Volatility pairlist filter """ import logging from copy import deepcopy From 9772a93634a1c60be321cbb78a51cec52cd59a5c Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 23:11:40 +0200 Subject: [PATCH 097/460] resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 ++--- tests/plugins/test_pairlist.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 5b50e04e4..6ef3841f5 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -16,7 +16,6 @@ from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList - logger = logging.getLogger(__name__) @@ -58,7 +57,8 @@ class VolatilityFilter(IPairList): Short whitelist method description - used for startup-messages """ return (f"{self.name} - Filtering pairs with volatility range " - f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + f"{self._min_volatility}-{self._max_volatility} " + f" the last {self._days} {plural(self._days, 'day')}.") def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ @@ -118,4 +118,3 @@ class VolatilityFilter(IPairList): self._pair_cache[pair] = result return result - \ No newline at end of file diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 7d39014f1..4db0b7098 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -419,7 +419,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t whitelist_conf['stake_currency'] = base_currency ohlcv_history_high_vola = ohlcv_history.copy() - ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index==1, 'close'] = 0.00090 + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090 ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, @@ -428,15 +428,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ('XRP/BTC', '1d'): ohlcv_history, ('HOT/BTC', '1d'): ohlcv_history_high_vola, } - - - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) if whitelist_result == 'static_in_the_middle': with pytest.raises(OperationalException, - match=r"StaticPairList can only be used in the first position " + match=r"StaticPairList only in the first position " r"in the list of Pairlist Handlers."): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return @@ -500,6 +497,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if pairlist["method"] == 'VolatilityFilter': assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) + def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] From 0f0607baecdbbc04378d546239265d2e22c52c02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:52:34 +0200 Subject: [PATCH 098/460] Fix rangeestability filter caching issue --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index a1430a223..6565e92c1 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -87,8 +87,9 @@ class RangeStabilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] + cached_res = self._pair_cache.get(pair, None) + if cached_res is not None: + return cached_res result = False if daily_candles is not None and not daily_candles.empty: From ac6bff536f8c5d5bc78b4cd4cdd499bba7af2e89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:55:11 +0200 Subject: [PATCH 099/460] Fix test failure with UI test if UI is deployed --- freqtrade/rpc/api_server/web_ui.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 13d22a63e..a8c737e04 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -13,6 +13,11 @@ async def favicon(): return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico')) +@router_ui.get('/fallback_file.html', include_in_schema=False) +async def fallback(): + return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html')) + + @router_ui.get('/{rest_of_path:path}', include_in_schema=False) async def index_html(rest_of_path: str): """ From d2680f6cb8c63de6470864b675fccdcb888ff01d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:57:05 +0200 Subject: [PATCH 100/460] Remove telegram deprecation warning closes #4688 --- freqtrade/rpc/telegram.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b418c3dab..17ddd1c91 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -159,10 +159,10 @@ class Telegram(RPCHandler): for handle in handles: self._updater.dispatcher.add_handler(handle) self._updater.start_polling( - clean=True, bootstrap_retries=-1, timeout=30, read_latency=60, + drop_pending_updates=True, ) logger.info( 'rpc.telegram is listening for following commands: %s', diff --git a/setup.py b/setup.py index bf23fb999..54a2e01b5 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup(name='freqtrade', # from requirements.txt 'ccxt>=1.24.96', 'SQLAlchemy', - 'python-telegram-bot', + 'python-telegram-bot>=13.4', 'arrow>=0.17.0', 'cachetools', 'requests', From 7f8d90d34c37549937dd4ceb2fde565b63c03213 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 07:05:10 +0200 Subject: [PATCH 101/460] Update list-exchanges doc with new format --- docs/utils.md | 197 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index a84f068e9..8ef12e1c9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -253,13 +253,206 @@ optional arguments: * Example: see exchanges available for the bot: ``` $ freqtrade list-exchanges -Exchanges available for Freqtrade: _1btcxe, acx, allcoin, bequant, bibox, binance, binanceje, binanceus, bitbank, bitfinex, bitfinex2, bitkk, bitlish, bitmart, bittrex, bitz, bleutrade, btcalpha, btcmarkets, btcturk, buda, cex, cobinhood, coinbaseprime, coinbasepro, coinex, cointiger, coss, crex24, digifinex, dsx, dx, ethfinex, fcoin, fcoinjp, gateio, gdax, gemini, hitbtc2, huobipro, huobiru, idex, kkex, kraken, kucoin, kucoin2, kuna, lbank, mandala, mercado, oceanex, okcoincny, okcoinusd, okex, okex3, poloniex, rightbtc, theocean, tidebit, upbit, zb +Exchanges available for Freqtrade: +Exchange name Valid reason +--------------- ------- -------------------------------------------- +aax True +ascendex True missing opt: fetchMyTrades +bequant True +bibox True +bigone True +binance True +binanceus True +bitbank True missing opt: fetchTickers +bitcoincom True +bitfinex True +bitforex True missing opt: fetchMyTrades, fetchTickers +bitget True +bithumb True missing opt: fetchMyTrades +bitkk True missing opt: fetchMyTrades +bitmart True +bitmax True missing opt: fetchMyTrades +bitpanda True +bittrex True +bitvavo True +bitz True missing opt: fetchMyTrades +btcalpha True missing opt: fetchTicker, fetchTickers +btcmarkets True missing opt: fetchTickers +buda True missing opt: fetchMyTrades, fetchTickers +bw True missing opt: fetchMyTrades, fetchL2OrderBook +bybit True +bytetrade True +cdax True +cex True missing opt: fetchMyTrades +coinbaseprime True missing opt: fetchTickers +coinbasepro True missing opt: fetchTickers +coinex True +crex24 True +deribit True +digifinex True +equos True missing opt: fetchTicker, fetchTickers +eterbase True +fcoin True missing opt: fetchMyTrades, fetchTickers +fcoinjp True missing opt: fetchMyTrades, fetchTickers +ftx True +gateio True +gemini True +gopax True +hbtc True +hitbtc True +huobijp True +huobipro True +idex True +kraken True +kucoin True +lbank True missing opt: fetchMyTrades +mercado True missing opt: fetchTickers +ndax True missing opt: fetchTickers +novadax True +okcoin True +okex True +probit True +qtrade True +stex True +timex True +upbit True missing opt: fetchMyTrades +vcc True +zb True missing opt: fetchMyTrades + ``` +!!! Note "missing opt exchanges" + Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will). + * Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): ``` $ freqtrade list-exchanges -a -All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpro, bcex, bequant, bibox, bigone, binance, binanceje, binanceus, bit2c, bitbank, bitbay, bitfinex, bitfinex2, bitflyer, bitforex, bithumb, bitkk, bitlish, bitmart, bitmex, bitso, bitstamp, bitstamp1, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, cex, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, deribit, digifinex, dsx, dx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, gateio, gdax, gemini, hitbtc, hitbtc2, huobipro, huobiru, ice3x, idex, independentreserve, indodax, itbit, kkex, kraken, kucoin, kucoin2, kuna, lakebtc, latoken, lbank, liquid, livecoin, luno, lykke, mandala, mercado, mixcoins, negociecoins, nova, oceanex, okcoincny, okcoinusd, okex, okex3, paymium, poloniex, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, upbit, vaultoro, vbtc, virwox, xbtce, yobit, zaif, zb +All exchanges supported by the ccxt library: +Exchange name Valid reason +------------------ ------- --------------------------------------------------------------------------------------- +aax True +aofex False missing: fetchOrder +ascendex True missing opt: fetchMyTrades +bequant True +bibox True +bigone True +binance True +binanceus True +bit2c False missing: fetchOrder, fetchOHLCV +bitbank True missing opt: fetchTickers +bitbay False missing: fetchOrder +bitcoincom True +bitfinex True +bitfinex2 False missing: fetchOrder +bitflyer False missing: fetchOrder, fetchOHLCV +bitforex True missing opt: fetchMyTrades, fetchTickers +bitget True +bithumb True missing opt: fetchMyTrades +bitkk True missing opt: fetchMyTrades +bitmart True +bitmax True missing opt: fetchMyTrades +bitmex False Various reasons. +bitpanda True +bitso False missing: fetchOHLCV +bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983 +bitstamp1 False missing: fetchOrder, fetchOHLCV +bittrex True +bitvavo True +bitz True missing opt: fetchMyTrades +bl3p False missing: fetchOrder, fetchOHLCV +bleutrade False missing: fetchOrder +braziliex False missing: fetchOHLCV +btcalpha True missing opt: fetchTicker, fetchTickers +btcbox False missing: fetchOHLCV +btcmarkets True missing opt: fetchTickers +btctradeua False missing: fetchOrder, fetchOHLCV +btcturk False missing: fetchOrder +buda True missing opt: fetchMyTrades, fetchTickers +bw True missing opt: fetchMyTrades, fetchL2OrderBook +bybit True +bytetrade True +cdax True +cex True missing opt: fetchMyTrades +chilebit False missing: fetchOrder, fetchOHLCV +coinbase False missing: fetchOrder, cancelOrder, createOrder, fetchOHLCV +coinbaseprime True missing opt: fetchTickers +coinbasepro True missing opt: fetchTickers +coincheck False missing: fetchOrder, fetchOHLCV +coinegg False missing: fetchOHLCV +coinex True +coinfalcon False missing: fetchOHLCV +coinfloor False missing: fetchOrder, fetchOHLCV +coingi False missing: fetchOrder, fetchOHLCV +coinmarketcap False missing: fetchOrder, cancelOrder, createOrder, fetchBalance, fetchOHLCV +coinmate False missing: fetchOHLCV +coinone False missing: fetchOHLCV +coinspot False missing: fetchOrder, cancelOrder, fetchOHLCV +crex24 True +currencycom False missing: fetchOrder +delta False missing: fetchOrder +deribit True +digifinex True +equos True missing opt: fetchTicker, fetchTickers +eterbase True +exmo False missing: fetchOrder +exx False missing: fetchOHLCV +fcoin True missing opt: fetchMyTrades, fetchTickers +fcoinjp True missing opt: fetchMyTrades, fetchTickers +flowbtc False missing: fetchOrder, fetchOHLCV +foxbit False missing: fetchOrder, fetchOHLCV +ftx True +gateio True +gemini True +gopax True +hbtc True +hitbtc True +hollaex False missing: fetchOrder +huobijp True +huobipro True +idex True +independentreserve False missing: fetchOHLCV +indodax False missing: fetchOHLCV +itbit False missing: fetchOHLCV +kraken True +kucoin True +kuna False missing: fetchOHLCV +lakebtc False missing: fetchOrder, fetchOHLCV +latoken False missing: fetchOrder, fetchOHLCV +lbank True missing opt: fetchMyTrades +liquid False missing: fetchOHLCV +luno False missing: fetchOHLCV +lykke False missing: fetchOHLCV +mercado True missing opt: fetchTickers +mixcoins False missing: fetchOrder, fetchOHLCV +ndax True missing opt: fetchTickers +novadax True +oceanex False missing: fetchOHLCV +okcoin True +okex True +paymium False missing: fetchOrder, fetchOHLCV +phemex False Does not provide history. +poloniex False missing: fetchOrder +probit True +qtrade True +rightbtc False missing: fetchOrder +ripio False missing: fetchOHLCV +southxchange False missing: fetchOrder, fetchOHLCV +stex True +surbitcoin False missing: fetchOrder, fetchOHLCV +therock False missing: fetchOHLCV +tidebit False missing: fetchOrder +tidex False missing: fetchOHLCV +timex True +upbit True missing opt: fetchMyTrades +vbtc False missing: fetchOrder, fetchOHLCV +vcc True +wavesexchange False missing: fetchOrder +whitebit False missing: fetchOrder, cancelOrder, createOrder, fetchBalance +xbtce False missing: fetchOrder, fetchOHLCV +xena False missing: fetchOrder +yobit False missing: fetchOHLCV +zaif False missing: fetchOrder, fetchOHLCV +zb True missing opt: fetchMyTrades ``` ## List Timeframes From 17508efbbc88e89a7380cb6d14f12c5350e0ed5c Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 08:59:44 +0200 Subject: [PATCH 102/460] resolves freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 33 ++++++++++++++++++++++++++++++++- tests/plugins/test_pairlist.py | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 2653406e7..ad1ac6efc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -4,7 +4,7 @@ Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). -Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. +Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. @@ -29,6 +29,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) * [`RangeStabilityFilter`](#rangestabilityfilter) +* [`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. @@ -164,6 +165,29 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit !!! Tip 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. +#### VolatilityFilter + +Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. + +Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. + +This filter can be used to narrow down your pairs to a certain volatilty or avoid very volatile pairs. + +In the below example: +If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. + +```json +"pairlists": [ + { + "method": "VolatilityFilter", + "lookback_days": 10, + "min_volatilty": 0.20, + "max_volatilty": 0.30, + "refresh_period": 86400 + } +] +``` + ### 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. @@ -189,6 +213,13 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "min_rate_of_change": 0.01, "refresh_period": 1440 }, + { + "method": "VolatilityFilter", + "lookback_days": 10, + "min_volatilty": 0.20, + "max_volatilty": 0.30, + "refresh_period": 86400 + }, {"method": "ShuffleFilter", "seed": 42} ], ``` diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 4db0b7098..bf225271f 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -433,7 +433,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if whitelist_result == 'static_in_the_middle': with pytest.raises(OperationalException, - match=r"StaticPairList only in the first position " + match=r"StaticPairList can only be used in the first position " r"in the list of Pairlist Handlers."): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return From 5ee879a747a5bbb916601c90af1e0cb25de56515 Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 10:15:51 +0200 Subject: [PATCH 103/460] isort resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 6ef3841f5..f8e380f56 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -2,14 +2,14 @@ Volatility pairlist filter """ import logging +import sys from copy import deepcopy from typing import Any, Dict, List, Optional -import sys import arrow +import numpy as np from cachetools.ttl import TTLCache from pandas import DataFrame -import numpy as np from freqtrade.exceptions import OperationalException from freqtrade.misc import plural From 4d30c32ad2bda337da4efb1f2d87bb76dc9693e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 17:10:20 +0200 Subject: [PATCH 104/460] Improve resiliancy of a test --- tests/conftest_trades.py | 4 +- tests/rpc/test_rpc_apiserver.py | 72 ++++++++++++++++----------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 8e4be9165..34fc58aee 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -241,7 +241,8 @@ def mock_trade_5(fee): open_rate=0.123, exchange='bittrex', strategy='SampleStrategy', - stoploss_order_id='prod_stoploss_3455' + stoploss_order_id='prod_stoploss_3455', + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') trade.orders.append(o) @@ -295,6 +296,7 @@ def mock_trade_6(fee): exchange='bittrex', strategy='SampleStrategy', open_order_id="prod_sell_6", + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..d113a8802 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -753,25 +753,21 @@ def test_api_status(botclient, mocker, ticker, fee, markets): get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) + markets=PropertyMock(return_value=markets), + fetch_order=MagicMock(return_value={}), ) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json() == [] - - ftbot.enter_positions() - trades = Trade.get_open_trades() - trades[0].open_order_id = None - ftbot.exit_positions(trades) - Trade.query.session.flush() + create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) - assert len(rc.json()) == 1 - assert rc.json() == [{ - 'amount': 91.07468123, - 'amount_requested': 91.07468123, + assert len(rc.json()) == 4 + assert rc.json()[0] == { + 'amount': 123.0, + 'amount_requested': 123.0, 'base_currency': 'BTC', 'close_date': None, 'close_date_hum': None, @@ -780,37 +776,37 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate': None, - 'current_profit': -0.00408133, - 'current_profit_pct': -0.41, - 'current_profit_abs': -4.09e-06, - 'profit_ratio': -0.00408133, - 'profit_pct': -0.41, - 'profit_abs': -4.09e-06, + 'current_profit': ANY, + 'current_profit_pct': ANY, + 'current_profit_abs': ANY, + 'profit_ratio': ANY, + 'profit_pct': ANY, + 'profit_abs': ANY, 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, - 'open_date_hum': 'just now', + 'open_date_hum': ANY, 'open_timestamp': ANY, 'open_order': None, - 'open_rate': 1.098e-05, + 'open_rate': 0.123, 'pair': 'ETH/BTC', 'stake_amount': 0.001, - 'stop_loss_abs': 9.882e-06, - 'stop_loss_pct': -10.0, - 'stop_loss_ratio': -0.1, + 'stop_loss_abs': ANY, + 'stop_loss_pct': ANY, + 'stop_loss_ratio': ANY, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss_abs': 9.882e-06, - 'initial_stop_loss_pct': -10.0, - 'initial_stop_loss_ratio': -0.1, - 'stoploss_current_dist': -1.1080000000000002e-06, - 'stoploss_current_dist_ratio': -0.10081893, - 'stoploss_current_dist_pct': -10.08, - 'stoploss_entry_dist': -0.00010475, - 'stoploss_entry_dist_ratio': -0.10448878, + 'initial_stop_loss_abs': 0.0, + 'initial_stop_loss_pct': ANY, + 'initial_stop_loss_ratio': ANY, + 'stoploss_current_dist': ANY, + 'stoploss_current_dist_ratio': ANY, + 'stoploss_current_dist_pct': ANY, + 'stoploss_entry_dist': ANY, + 'stoploss_entry_dist_ratio': ANY, 'trade_id': 1, - 'close_rate_requested': None, + 'close_rate_requested': ANY, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, @@ -818,17 +814,17 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': True, - 'max_rate': 1.099e-05, - 'min_rate': 1.098e-05, - 'open_order_id': None, - 'open_rate_requested': 1.098e-05, - 'open_trade_value': 0.0010025, + 'max_rate': ANY, + 'min_rate': ANY, + 'open_order_id': 'dry_run_buy_12345', + 'open_rate_requested': ANY, + 'open_trade_value': 15.1668225, 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, 'exchange': 'bittrex', - }] + } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) @@ -836,7 +832,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) resp_values = rc.json() - assert len(resp_values) == 1 + assert len(resp_values) == 4 assert isnan(resp_values[0]['profit_abs']) From f8244d9d76610c96e0995754a934272bf94a536f Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 22:25:54 +0200 Subject: [PATCH 105/460] resolves freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 6 +++--- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ad1ac6efc..8c65753b6 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,7 +167,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. +Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. @@ -181,8 +181,8 @@ If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove { "method": "VolatilityFilter", "lookback_days": 10, - "min_volatilty": 0.20, - "max_volatilty": 0.30, + "min_volatility": 0.05, + "max_volatility": 0.50, "refresh_period": 86400 } ] diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index f8e380f56..400b1577d 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -94,8 +94,9 @@ class VolatilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] + cached_res = self._pair_cache.get(pair, None) + if cached_res is not None: + return cached_res result = False if daily_candles is not None and not daily_candles.empty: From 862f69f895c4dd1774ba78ca9a221692ad9bb930 Mon Sep 17 00:00:00 2001 From: gbojen Date: Thu, 8 Apr 2021 16:43:38 +0200 Subject: [PATCH 106/460] removed typos --- docs/includes/pairlists.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8c65753b6..3aa2c63f4 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,14 +167,14 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). -Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. +This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. -This filter can be used to narrow down your pairs to a certain volatilty or avoid very volatile pairs. +This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: -If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. +If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. ```json "pairlists": [ From 5a5c5fccf236bf23612af897527fb7f86f588222 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 16:45:52 +0200 Subject: [PATCH 107/460] Add gitattributes file --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..00abd1d9d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.py eol=lf +*.sh eol=lf +*.ps1 eol=crlf From 74bf0b6399adf9766f8af6b2c68140723d0c3d51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 19:29:51 +0200 Subject: [PATCH 108/460] Fix typo in documentation --- docs/includes/pairlists.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 3aa2c63f4..d57757bbd 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,7 +167,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. @@ -216,8 +216,8 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, { "method": "VolatilityFilter", "lookback_days": 10, - "min_volatilty": 0.20, - "max_volatilty": 0.30, + "min_volatility": 0.05, + "max_volatility": 0.50, "refresh_period": 86400 }, {"method": "ShuffleFilter", "seed": 42} From 898c24949bfcaee24e08198566afb15125679c4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:07:52 +0200 Subject: [PATCH 109/460] Add chown method to support docker --- freqtrade/commands/build_config_commands.py | 2 ++ .../configuration/directory_operations.py | 16 +++++++++++++ tests/test_directory_operations.py | 23 +++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 0dee480b3..03d095e12 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List from questionary import Separator, prompt +from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import OperationalException from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges @@ -216,6 +217,7 @@ def start_new_config(args: Dict[str, Any]) -> None: """ config_path = Path(args['config'][0]) + chown_user_directory(config_path.parent) if config_path.exists(): overwrite = ask_user_overwrite(config_path) if overwrite: diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 1ce8d1461..ca305c260 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -24,6 +24,21 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat return folder +def chown_user_directory(directory: Path) -> None: + """ + Use Sudo to change permissions of the home-directory if necessary + Only applies when running in docker! + """ + import os + if os.environ.get('FT_APP_ENV') == 'docker': + try: + import subprocess + subprocess.check_output( + ['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())]) + except Exception: + logger.warning(f"Could not chown {directory}") + + def create_userdata_dir(directory: str, create_dir: bool = False) -> Path: """ Create userdata directory structure. @@ -37,6 +52,7 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path: sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "logs", "notebooks", "plot", "strategies", ] folder = Path(directory) + chown_user_directory(folder) if not folder.is_dir(): if create_dir: folder.mkdir(parents=True) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index a8058c514..a11200526 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -1,11 +1,12 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name +import os from pathlib import Path from unittest.mock import MagicMock import pytest -from freqtrade.configuration.directory_operations import (copy_sample_files, create_datadir, - create_userdata_dir) +from freqtrade.configuration.directory_operations import (chown_user_directory, copy_sample_files, + create_datadir, create_userdata_dir) from freqtrade.exceptions import OperationalException from tests.conftest import log_has, log_has_re @@ -31,6 +32,24 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: assert str(x) == str(Path("/tmp/bar")) +def test_create_userdata_dir_and_chown(mocker, tmpdir, caplog) -> None: + sp_mock = mocker.patch('subprocess.check_output') + path = Path(tmpdir / 'bar') + assert not path.is_dir() + + x = create_userdata_dir(str(path), create_dir=True) + assert sp_mock.call_count == 0 + assert log_has(f'Created user-data directory: {path}', caplog) + assert isinstance(x, Path) + assert path.is_dir() + assert (path / 'data').is_dir() + + os.environ['FT_APP_ENV'] = 'docker' + chown_user_directory(path / 'data') + assert sp_mock.call_count == 1 + del os.environ['FT_APP_ENV'] + + def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) md = mocker.patch.object(Path, 'mkdir', MagicMock()) From 4eb251ce416cf3a3bf40071b8843814f38726cfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:17:53 +0200 Subject: [PATCH 110/460] Update dockerfiles to run as non-root --- Dockerfile | 26 ++++++++++++++++++-------- Dockerfile.armhf | 28 +++++++++++++++++----------- docker/Dockerfile.custom | 8 ++++---- docker/Dockerfile.develop | 4 ++-- docker/Dockerfile.jupyter | 2 +- docker/Dockerfile.plot | 2 +- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b399174b..ac48ea611 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,19 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" # Prepare environment -RUN mkdir /freqtrade +RUN mkdir /freqtrade \ + && apt update \ + && apt install -y sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + WORKDIR /freqtrade # Install dependencies @@ -24,7 +33,8 @@ 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/ +COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ +USER ftuser RUN pip install --user --no-cache-dir numpy \ && pip install --user --no-cache-dir -r requirements-hyperopt.txt @@ -33,13 +43,13 @@ 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 - - +COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +USER ftuser # Install and execute -COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir \ +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ && mkdir /freqtrade/user_data/ \ && freqtrade install-ui diff --git a/Dockerfile.armhf b/Dockerfile.armhf index eecd9fdc0..62ef165c0 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -5,15 +5,20 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" # Prepare environment -RUN mkdir /freqtrade -WORKDIR /freqtrade +RUN mkdir /freqtrade \ + && apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -RUN apt-get update \ - && apt-get -y install libatlas3-base curl sqlite3 \ - && apt-get clean +WORKDIR /freqtrade # Install dependencies FROM base as python-deps @@ -37,13 +42,14 @@ 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 +COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +USER ftuser # Install and execute -COPY . /freqtrade/ -RUN apt-get install -y libhdf5-serial-dev \ - && apt-get clean \ - && pip install -e . --no-cache-dir \ +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ + && mkdir /freqtrade/user_data/ \ && freqtrade install-ui ENTRYPOINT ["freqtrade"] diff --git a/docker/Dockerfile.custom b/docker/Dockerfile.custom index 10620e6b8..a7c599fa8 100644 --- a/docker/Dockerfile.custom +++ b/docker/Dockerfile.custom @@ -1,7 +1,7 @@ FROM freqtradeorg/freqtrade:develop -RUN apt-get update \ - && apt-get -y install git \ - && apt-get clean \ +RUN sudo apt-get update \ + && sudo apt-get -y install git \ + && sudo apt-get clean \ # The below dependency - pyti - serves as an example. Please use whatever you need! - && pip install pyti + && pip install --user pyti diff --git a/docker/Dockerfile.develop b/docker/Dockerfile.develop index cb49984e2..7c580f234 100644 --- a/docker/Dockerfile.develop +++ b/docker/Dockerfile.develop @@ -3,8 +3,8 @@ FROM freqtradeorg/freqtrade:develop # Install dependencies COPY requirements-dev.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements-dev.txt --no-cache-dir +RUN pip install numpy --user --no-cache-dir \ + && pip install -r requirements-dev.txt --user --no-cache-dir # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/docker/Dockerfile.jupyter b/docker/Dockerfile.jupyter index b7499eeef..7d603c667 100644 --- a/docker/Dockerfile.jupyter +++ b/docker/Dockerfile.jupyter @@ -1,7 +1,7 @@ FROM freqtradeorg/freqtrade:develop_plot -RUN pip install jupyterlab --no-cache-dir +RUN pip install jupyterlab --user --no-cache-dir # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 40bc72bc5..d2fc3618a 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -4,4 +4,4 @@ FROM freqtradeorg/freqtrade:${sourceimage} # Install dependencies COPY requirements-plot.txt /freqtrade/ -RUN pip install -r requirements-plot.txt --no-cache-dir +RUN pip install -r requirements-plot.txt --user --no-cache-dir From 644dcc1641624700033b0865f8360a5264e51585 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:36:10 +0200 Subject: [PATCH 111/460] Only allow chown via sudo --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- docker/Dockerfile.custom | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index ac48ea611..a6dc9a991 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN mkdir /freqtrade \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers WORKDIR /freqtrade diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 62ef165c0..909c44eaa 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -16,7 +16,7 @@ RUN mkdir /freqtrade \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers WORKDIR /freqtrade diff --git a/docker/Dockerfile.custom b/docker/Dockerfile.custom index a7c599fa8..3b55fcb0e 100644 --- a/docker/Dockerfile.custom +++ b/docker/Dockerfile.custom @@ -1,7 +1,10 @@ FROM freqtradeorg/freqtrade:develop -RUN sudo apt-get update \ - && sudo apt-get -y install git \ - && sudo apt-get clean \ - # The below dependency - pyti - serves as an example. Please use whatever you need! - && pip install --user pyti +# Switch user to root if you must install something from apt +# Don't forget to switch the user back below! +# USER root + +# The below dependency - pyti - serves as an example. Please use whatever you need! +RUN pip install --user pyti + +# USER ftuser From 0b4b67e46b444167d18d37657521d7c212781c92 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Fri, 9 Apr 2021 10:36:03 +0900 Subject: [PATCH 112/460] add FAQ entries for shorting, futures, and options --- docs/faq.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 93b806dca..c890caf1a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,5 +1,19 @@ # Freqtrade FAQ +## Supported Markets + +Freqtrade supports spot trading only. + +### Can I open short positions? + +No, Freqtrade does not support trading with margin / leverage, and cannot open short positions. + +In some cases, your exchange may provide leveraged spot tokens which can be traded with Freqtrade eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD, etc... + +### Can I trade options or futures? + +No, options and futures trading are not supported. + ## 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). From 4b2cec22ec645bd8a2ef0d75db1a3f2f0b586b1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 19:33:40 +0200 Subject: [PATCH 113/460] Chown .local dir --- .dockerignore | 12 ++++++++++-- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 09f4c9f0c..889a4dfc7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,8 @@ .git .gitignore Dockerfile +Dockerfile.armhf .dockerignore -config.json* -*.sqlite .coveragerc .eggs .github @@ -13,4 +12,13 @@ CONTRIBUTING.md MANIFEST.in README.md freqtrade.service +freqtrade.egg-info + +config.json* +*.sqlite user_data +*.log + +.vscode +.mypy_cache +.ipynb_checkpoints diff --git a/Dockerfile b/Dockerfile index a6dc9a991..5d8bcdd41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ 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 /home/ftuser/.local /home/ftuser/.local +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local USER ftuser # Install and execute diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 909c44eaa..df1c4bc80 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -42,7 +42,7 @@ 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 /home/ftuser/.local /home/ftuser/.local +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local USER ftuser # Install and execute From 126127c1e11a09a4d4653c0ca9551ffc8cf35ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 21:28:38 +0200 Subject: [PATCH 114/460] Fix armHF image to use ftuser on install too --- Dockerfile.armhf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index df1c4bc80..332b91b35 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -33,7 +33,8 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt /freqtrade/ +COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ +USER ftuser RUN pip install --user --no-cache-dir numpy \ && pip install --user --no-cache-dir -r requirements.txt From 5f67400649d40637177a4a5071e364e94cdea0e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 21:58:15 +0200 Subject: [PATCH 115/460] Add SKDecimal Space --- freqtrade/optimize/decimalspace.py | 33 ++++++++++++++++++++++++++++++ freqtrade/strategy/hyper.py | 16 ++++----------- 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 freqtrade/optimize/decimalspace.py diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py new file mode 100644 index 000000000..fbc6a7af6 --- /dev/null +++ b/freqtrade/optimize/decimalspace.py @@ -0,0 +1,33 @@ +import numpy as np + +from skopt.space import Integer + + +class SKDecimal(Integer): + + def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None, + name=None, dtype=np.int64): + self.decimals = decimals + _low = int(low * pow(10, self.decimals)) + _high = int(high * pow(10, self.decimals)) + self.low_orig = low + self.high_orig = high + + super().__init__(_low, _high, prior, base, transform, name, dtype) + + def __repr__(self): + return "Decimal(low={}, high={}, decimals={}, prior='{}', transform='{}')".format( + self.low_orig, self.high_orig, self.decimals, self.prior, self.transform_) + + def __contains__(self, point): + if isinstance(point, list): + point = np.array(point) + return self.low_orig <= point <= self.high_orig + + def transform(self, Xt): + aa = [int(x * pow(10, self.decimals)) for x in Xt] + return super().transform(aa) + + def inverse_transform(self, Xt): + res = super().inverse_transform(Xt) + return [round(x * pow(0.1, self.decimals), self.decimals) for x in res] diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 709179997..9a7fc4ba4 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,6 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical + from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.exceptions import OperationalException @@ -168,22 +169,13 @@ class DecimalParameter(RealParameter): super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) - def get_space(self, name: str) -> 'Integer': + def get_space(self, name: str) -> 'SKDecimal': """ Create skopt optimization space. :param name: A name of parameter field. """ - low = int(self.opt_range[0] * pow(10, self._decimals)) - high = int(self.opt_range[1] * pow(10, self._decimals)) - return Integer(low, high, name=name, **self._space_params) - - def _set_value(self, value: int): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: An integer value. - """ - self.value = round(value * pow(0.1, self._decimals), self._decimals) + return SKDecimal(*self.opt_range, decimals=self._decimals, name=name, + **self._space_params) class CategoricalParameter(BaseParameter): From fedff1a75ac7de025449c9fecd884cb8c17f80c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 22:10:20 +0200 Subject: [PATCH 116/460] Fix failing test --- tests/strategy/test_interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3bfa691b4..0ee80e0c5 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -596,8 +596,6 @@ def test_hyperopt_parameters(): fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') assert isinstance(fltpar.get_space(''), Integer) assert fltpar.value == 1 - fltpar._set_value(2222) - assert fltpar.value == 2.222 catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') From 34e47db18d4cd8bd44b1a8cd125a45c43c115dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 22:15:24 +0200 Subject: [PATCH 117/460] Test SKDecimal space --- freqtrade/optimize/decimalspace.py | 1 - tests/optimize/test_hyperopt.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py index fbc6a7af6..f5370b6d6 100644 --- a/freqtrade/optimize/decimalspace.py +++ b/freqtrade/optimize/decimalspace.py @@ -1,5 +1,4 @@ import numpy as np - from skopt.space import Integer diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c13da0d76..129fe53d9 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException +from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -1104,3 +1105,20 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) hyperopt.start() + + +def test_SKDecimal(): + space = SKDecimal(1, 2, decimals=2) + assert 1.5 in space + assert 2.5 not in space + assert space.low == 100 + assert space.high == 200 + + assert space.inverse_transform([200]) == [2.0] + assert space.inverse_transform([100]) == [1.0] + assert space.inverse_transform([150, 160]) == [1.5, 1.6] + + assert space.transform([1.5]) == [150] + assert space.transform([2.0]) == [200] + assert space.transform([1.0]) == [100] + assert space.transform([1.5, 1.6]) == [150, 160] From ea4b5d675df701d9cb0d6b36ada7cd9023408c97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 07:12:31 +0200 Subject: [PATCH 118/460] Don't explode low/high, but use explicit parameters --- freqtrade/strategy/hyper.py | 48 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 9a7fc4ba4..c72beba55 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -25,9 +25,8 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - opt_range: Sequence[Any] - def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, + def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. @@ -44,7 +43,6 @@ class BaseParameter(ABC): self.category = space self._space_params = kwargs self.value = default - self.opt_range = opt_range self.optimize = optimize self.load = load @@ -69,7 +67,6 @@ class BaseParameter(ABC): class IntParameter(BaseParameter): default: int value: int - opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -90,10 +87,12 @@ class IntParameter(BaseParameter): if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') - opt_range = low + self.low, self.high = low else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -101,13 +100,12 @@ class IntParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.opt_range, name=name, **self._space_params) + return Integer(low=self.low, high=self.high, name=name, **self._space_params) class RealParameter(BaseParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, optimize: bool = True, @@ -129,10 +127,11 @@ class RealParameter(BaseParameter): if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - opt_range = low + self.low, self.high = low else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + self.low = low + self.high = high + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -140,13 +139,12 @@ class RealParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.opt_range, name=name, **self._space_params) + return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(RealParameter): +class DecimalParameter(BaseParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, decimals: int = 3, space: Optional[str] = None, @@ -162,11 +160,22 @@ class DecimalParameter(RealParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param optimize: Include parameter in hyperopt optimizations. :param load: Load parameter value from {space}_params. - :param kwargs: Extra parameters to skopt.space.Real. + :param kwargs: Extra parameters to skopt.space.Integer. """ self._decimals = decimals default = round(default, self._decimals) - super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, + + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'SKDecimal': @@ -174,7 +183,7 @@ class DecimalParameter(RealParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return SKDecimal(*self.opt_range, decimals=self._decimals, name=name, + return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name, **self._space_params) @@ -200,7 +209,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + self.opt_range = categories + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': From 83fbaf16c84d6395048e62956adf6509a8a70939 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 07:57:02 +0200 Subject: [PATCH 119/460] Extract numeric param validation and explosion --- freqtrade/strategy/hyper.py | 76 ++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c72beba55..35000d916 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -64,7 +64,43 @@ class BaseParameter(ABC): self.value = value -class IntParameter(BaseParameter): +class NumericParameter(BaseParameter): + """ Internal parameter used for Numeric purposes """ + float_or_int = Union[int, float] + default: float_or_int + value: float_or_int + + def __init__(self, low: Union[float_or_int, Sequence[float_or_int]], + high: Optional[float_or_int] = None, *, default: float_or_int, + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable numeric parameter. + Cannot be instantiated, but provides the validation for other numeric parameters + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. + :param default: A default value. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.*. + """ + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, + load=load, **kwargs) + + +class IntParameter(NumericParameter): default: int value: int @@ -82,17 +118,8 @@ class IntParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException('IntParameter space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -103,7 +130,7 @@ class IntParameter(BaseParameter): return Integer(low=self.low, high=self.high, name=name, **self._space_params) -class RealParameter(BaseParameter): +class RealParameter(NumericParameter): default: float value: float @@ -122,16 +149,7 @@ class RealParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -142,7 +160,7 @@ class RealParameter(BaseParameter): return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(BaseParameter): +class DecimalParameter(NumericParameter): default: float value: float @@ -165,17 +183,7 @@ class DecimalParameter(BaseParameter): self._decimals = decimals default = round(default, self._decimals) - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'SKDecimal': From 9804e201146762f96752e18f41304b37bf47804a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 09:53:48 +0200 Subject: [PATCH 120/460] Don't use _set_value for autoOpt-Spaces --- freqtrade/optimize/hyperopt_auto.py | 4 ++-- freqtrade/strategy/hyper.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index c4d6f1581..f86204406 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -27,7 +27,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -37,7 +37,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_sell_trend diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 35000d916..3fedda974 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -50,19 +50,11 @@ class BaseParameter(ABC): return f'{self.__class__.__name__}({self.value})' @abstractmethod - def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: + def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ - def _set_value(self, value: Any): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: A numerical value. - """ - self.value = value - class NumericParameter(BaseParameter): """ Internal parameter used for Numeric purposes """ From ebbe47f38d884b139610d60db09708b32dcb6b71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 13:36:16 +0200 Subject: [PATCH 121/460] Simplify fiat convert and fix USD coingecko problem --- freqtrade/rpc/fiat_convert.py | 103 +++++++-------------------------- tests/rpc/test_fiat_convert.py | 78 +++---------------------- 2 files changed, 30 insertions(+), 151 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 4e26432d4..380070deb 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -4,9 +4,9 @@ e.g BTC to USD """ import logging -import time -from typing import Dict, List +from typing import Dict +from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI from freqtrade.constants import SUPPORTED_FIAT @@ -15,51 +15,6 @@ from freqtrade.constants import SUPPORTED_FIAT logger = logging.getLogger(__name__) -class CryptoFiat: - """ - Object to describe what is the price of Crypto-currency in a FIAT - """ - # Constants - CACHE_DURATION = 6 * 60 * 60 # 6 hours - - def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None: - """ - Create an object that will contains the price for a crypto-currency in fiat - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) - :param price: Price in FIAT - """ - - # Public attributes - self.crypto_symbol = None - self.fiat_symbol = None - self.price = 0.0 - - # Private attributes - self._expiration = 0.0 - - self.crypto_symbol = crypto_symbol.lower() - self.fiat_symbol = fiat_symbol.lower() - self.set_price(price=price) - - def set_price(self, price: float) -> None: - """ - Set the price of the Crypto-currency in FIAT and set the expiration time - :param price: Price of the current Crypto currency in the fiat - :return: None - """ - self.price = price - self._expiration = time.time() + self.CACHE_DURATION - - def is_expired(self) -> bool: - """ - Return if the current price is still valid or needs to be refreshed - :return: bool, true the price is expired and needs to be refreshed, false the price is - still valid - """ - return self._expiration - time.time() <= 0 - - class CryptoToFiatConverter: """ Main class to initiate Crypto to FIAT. @@ -84,7 +39,9 @@ class CryptoToFiatConverter: return CryptoToFiatConverter.__instance def __init__(self) -> None: - self._pairs: List[CryptoFiat] = [] + # Timeout: 6h + self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60) + self._load_cryptomap() def _load_cryptomap(self) -> None: @@ -118,49 +75,31 @@ class CryptoToFiatConverter: """ crypto_symbol = crypto_symbol.lower() fiat_symbol = fiat_symbol.lower() + inverse = False + if crypto_symbol == 'usd': + # usd corresponds to "uniswap-state-dollar" for coingecko. + # We'll therefore need to "swap" the currencies + logger.info(f"reversing Rates {crypto_symbol}, {fiat_symbol}") + crypto_symbol = fiat_symbol + fiat_symbol = 'usd' + inverse = True + + symbol = f"{crypto_symbol}/{fiat_symbol}" # Check if the fiat convertion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): raise ValueError(f'The fiat {fiat_symbol} is not supported.') - # Get the pair that interest us and return the price in fiat - for pair in self._pairs: - if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol: - # If the price is expired we refresh it, avoid to call the API all the time - if pair.is_expired(): - pair.set_price( - price=self._find_price( - crypto_symbol=pair.crypto_symbol, - fiat_symbol=pair.fiat_symbol - ) - ) + price = self._pair_price.get(symbol, None) - # return the last price we have for this pair - return pair.price - - # The pair does not exist, so we create it and return the price - return self._add_pair( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol, - price=self._find_price( + if not price: + price = self._find_price( crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol ) - ) - - def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float: - """ - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) - :return: price in FIAT - """ - self._pairs.append( - CryptoFiat( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol, - price=price - ) - ) + if inverse and price != 0.0: + price = 1 / price + self._pair_price[symbol] = price return price diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index ed21bc516..2d43addff 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,44 +1,15 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 -import time from unittest.mock import MagicMock import pytest from requests.exceptions import RequestException -from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter +from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from tests.conftest import log_has, log_has_re -def test_pair_convertion_object(): - pair_convertion = CryptoFiat( - crypto_symbol='btc', - fiat_symbol='usd', - price=12345.0 - ) - - # Check the cache duration is 6 hours - assert pair_convertion.CACHE_DURATION == 6 * 60 * 60 - - # Check a regular usage - assert pair_convertion.crypto_symbol == 'btc' - assert pair_convertion.fiat_symbol == 'usd' - assert pair_convertion.price == 12345.0 - assert pair_convertion.is_expired() is False - - # Update the expiration time (- 2 hours) and check the behavior - pair_convertion._expiration = time.time() - 2 * 60 * 60 - assert pair_convertion.is_expired() is True - - # Check set price behaviour - time_reference = time.time() + pair_convertion.CACHE_DURATION - pair_convertion.set_price(price=30000.123) - assert pair_convertion.is_expired() is False - assert pair_convertion._expiration >= time_reference - assert pair_convertion.price == 30000.123 - - def test_fiat_convert_is_supported(mocker): fiat_convert = CryptoToFiatConverter() assert fiat_convert._is_supported_fiat(fiat='USD') is True @@ -47,28 +18,6 @@ def test_fiat_convert_is_supported(mocker): assert fiat_convert._is_supported_fiat(fiat='ABC') is False -def test_fiat_convert_add_pair(mocker): - - fiat_convert = CryptoToFiatConverter() - - pair_len = len(fiat_convert._pairs) - assert pair_len == 0 - - fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0) - pair_len = len(fiat_convert._pairs) - assert pair_len == 1 - assert fiat_convert._pairs[0].crypto_symbol == 'btc' - assert fiat_convert._pairs[0].fiat_symbol == 'usd' - assert fiat_convert._pairs[0].price == 12345.0 - - fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2) - pair_len = len(fiat_convert._pairs) - assert pair_len == 2 - assert fiat_convert._pairs[1].crypto_symbol == 'btc' - assert fiat_convert._pairs[1].fiat_symbol == 'eur' - assert fiat_convert._pairs[1].price == 13000.2 - - def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() @@ -95,8 +44,8 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog): def test_fiat_convert_get_price(mocker): - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', - return_value=28000.0) + find_price = mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', + return_value=28000.0) fiat_convert = CryptoToFiatConverter() @@ -104,26 +53,17 @@ def test_fiat_convert_get_price(mocker): fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar') # Check the value return by the method - pair_len = len(fiat_convert._pairs) + pair_len = len(fiat_convert._pair_price) assert pair_len == 0 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 - assert fiat_convert._pairs[0].crypto_symbol == 'btc' - assert fiat_convert._pairs[0].fiat_symbol == 'usd' - assert fiat_convert._pairs[0].price == 28000.0 - assert fiat_convert._pairs[0]._expiration != 0 - assert len(fiat_convert._pairs) == 1 + assert fiat_convert._pair_price['btc/usd'] == 28000.0 + assert len(fiat_convert._pair_price) == 1 + assert find_price.call_count == 1 # Verify the cached is used - fiat_convert._pairs[0].price = 9867.543 - expiration = fiat_convert._pairs[0]._expiration + fiat_convert._pair_price['btc/usd'] = 9867.543 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 9867.543 - assert fiat_convert._pairs[0]._expiration == expiration - - # Verify the cache expiration - expiration = time.time() - 2 * 60 * 60 - fiat_convert._pairs[0]._expiration = expiration - assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 - assert fiat_convert._pairs[0]._expiration is not expiration + assert find_price.call_count == 1 def test_fiat_convert_same_currencies(mocker): From 37c2e037f11da94b7c55a567d30c6184830a3148 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 13:50:56 +0200 Subject: [PATCH 122/460] Rename dry_run_order to create_dry_run_order --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 8 ++++---- freqtrade/exchange/ftx.py | 2 +- freqtrade/exchange/kraken.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 26ec30a8a..0bcfa5e17 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -52,7 +52,7 @@ class Binance(Exchange): 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 37d92e253..7edace13c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -543,8 +543,8 @@ class Exchange: # See also #2575 at github. return max(min_stake_amounts) * amount_reserve_percent - def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order = { @@ -618,7 +618,7 @@ class Exchange: rate: float, time_in_force: str) -> Dict: if self._config['dry_run']: - dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, "buy", amount, rate) return dry_order params = self._params.copy() @@ -631,7 +631,7 @@ class Exchange: rate: float, time_in_force: str = 'gtc') -> Dict: if self._config['dry_run']: - dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, "sell", amount, rate) return dry_order params = self._params.copy() diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f05490cbb..6312759b9 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -53,7 +53,7 @@ class Ftx(Exchange): stop_price = self.price_to_precision(pair, stop_price) if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 724b11189..786f1b592 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -92,7 +92,7 @@ class Kraken(Exchange): stop_price = self.price_to_precision(pair, stop_price) if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3439c7a09..202f1885f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -931,11 +931,11 @@ def test_exchange_has(default_conf, mocker): ("sell") ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_dry_run_order(default_conf, mocker, side, exchange_name): +def test_create_dry_run_order(default_conf, mocker, side, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - order = exchange.dry_run_order( + order = exchange.create_dry_run_order( pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) assert 'id' in order assert f'dry_run_{side}_' in order["id"] From 14e857423528b87e0cbfd46f1e3ad2264cda8ccd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:13:00 +0200 Subject: [PATCH 123/460] fetch_balance is never called in dry-run --- freqtrade/exchange/exchange.py | 4 ---- tests/exchange/test_exchange.py | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7edace13c..3224255d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -662,8 +662,6 @@ class Exchange: @retrier def get_balance(self, currency: str) -> float: - if self._config['dry_run']: - return self._config['dry_run_wallet'] # ccxt exception is already handled by get_balances balances = self.get_balances() @@ -675,8 +673,6 @@ class Exchange: @retrier def get_balances(self) -> dict: - if self._config['dry_run']: - return {} try: balances = self._api.fetch_balance() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 202f1885f..4ceba6eba 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1245,14 +1245,6 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert "timeInForce" not in api_mock.create_order.call_args[0][5] -def test_get_balance_dry_run(default_conf, mocker): - default_conf['dry_run'] = True - default_conf['dry_run_wallet'] = 999.9 - - exchange = get_patched_exchange(mocker, default_conf) - assert exchange.get_balance(currency='BTC') == 999.9 - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balance_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() @@ -1276,13 +1268,6 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -@pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_balances_dry_run(default_conf, mocker, exchange_name): - default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - assert exchange.get_balances() == {} - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balances_prod(default_conf, mocker, exchange_name): balance_item = { From 96a5b6555dd6a2c4f7fc62112d618f7c1d928843 Mon Sep 17 00:00:00 2001 From: gbojen Date: Sat, 10 Apr 2021 14:31:12 +0200 Subject: [PATCH 124/460] fix documentation inconsistency fixes freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index d57757bbd..8688494cc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -174,7 +174,7 @@ This filter removes pairs if the average volatility over a `lookback_days` days This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: -If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. +If the volatility over the last 10 days is not in the range of 0.05-0.50, remove the pair from the whitelist. The filter is applied every 24h. ```json "pairlists": [ @@ -190,7 +190,7 @@ If the volatility over the last 10 days is not in the range of 0.20-0.30, remove ### 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. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. ```json "exchange": { From 579e68f31e122a3ebbd897371642a7ee299749c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:37:09 +0200 Subject: [PATCH 125/460] Reduce log verbosity when buying --- freqtrade/freqtradebot.py | 11 ++++------- tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a701e8db9..1ebf28ebd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -410,9 +410,7 @@ class FreqtradeBot(LoggingMixin): bid_strategy = self.config.get('bid_strategy', {}) if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): - logger.info( - f"Getting price from order book {bid_strategy['price_side'].capitalize()} side." - ) + order_book_top = bid_strategy.get('order_book_top', 1) order_book = self.exchange.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) @@ -425,7 +423,8 @@ class FreqtradeBot(LoggingMixin): f"Orderbook: {order_book}" ) raise PricingError from e - logger.info(f'...top {order_book_top} order book buy rate {rate_from_l2:.8f}') + logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " + f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") used_rate = rate_from_l2 else: logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") @@ -479,19 +478,17 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False - logger.info(f"Buy signal found: about create a new trade with stake_amount: " + logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): if self._check_depth_of_market_buy(pair, bid_check_dom): - logger.info(f'Executing Buy for {pair}.') return self.execute_buy(pair, stake_amount) else: return False - logger.info(f'Executing Buy for {pair}') return self.execute_buy(pair, stake_amount) else: return False diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c93f8b858..a7b9bb103 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -685,7 +685,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog + 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog ) From 4820b4b314096fc2529ad67f5d2dd2a8ccd431b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:52:34 +0200 Subject: [PATCH 126/460] Fix test failure --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a7b9bb103..c91015766 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -685,7 +685,8 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog + 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', + caplog ) From aaf9872ef37b5959ce1489da8470ce1dd534c2ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 19:53:00 +0200 Subject: [PATCH 127/460] Simplify webserver test --- tests/rpc/test_rpc_apiserver.py | 61 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d113a8802..e72749715 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -416,10 +416,10 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json()["max"] == 1 # Create some test data - ftbot.enter_positions() + create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) - assert rc.json()["current"] == 1 + assert rc.json()["current"] == 4 assert rc.json()["max"] == 1 ftbot.config['max_open_trades'] = float('inf') @@ -612,7 +612,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") -def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): +def test_api_profit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -627,48 +627,33 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert_response(rc, 200) assert rc.json()['trade_count'] == 0 - ftbot.enter_positions() - trade = Trade.query.first() - + create_mock_trades(fee) # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - rc = client_get(client, f"{BASE_URI}/profit") - assert_response(rc, 200) - # One open trade - assert rc.json()['trade_count'] == 1 - assert rc.json()['best_pair'] == '' - assert rc.json()['best_rate'] == 0 - - trade = Trade.query.first() - trade.update(limit_sell_order) - - trade.close_date = datetime.utcnow() - trade.is_open = False rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) assert rc.json() == {'avg_duration': ANY, - 'best_pair': 'ETH/BTC', - 'best_rate': 6.2, - 'first_trade_date': 'just now', + 'best_pair': 'XRP/BTC', + 'best_rate': 1.0, + 'first_trade_date': ANY, 'first_trade_timestamp': ANY, - 'latest_trade_date': 'just now', + 'latest_trade_date': '5 minutes ago', 'latest_trade_timestamp': ANY, - 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': 0.76748865, - 'profit_all_percent_mean': 6.2, - 'profit_all_ratio_mean': 0.06201058, - 'profit_all_percent_sum': 6.2, - 'profit_all_ratio_sum': 0.06201058, - 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': 0.76748865, - 'profit_closed_ratio_mean': 0.06201058, - 'profit_closed_percent_mean': 6.2, - 'profit_closed_ratio_sum': 0.06201058, - 'profit_closed_percent_sum': 6.2, - 'trade_count': 1, - 'closed_trade_count': 1, - 'winning_trades': 1, + 'profit_all_coin': -44.0631579, + 'profit_all_fiat': -543959.6842755, + 'profit_all_percent_mean': -66.41, + 'profit_all_ratio_mean': -0.6641100666666667, + 'profit_all_percent_sum': -398.47, + 'profit_all_ratio_sum': -3.9846604, + 'profit_closed_coin': 0.00073913, + 'profit_closed_fiat': 9.124559849999999, + 'profit_closed_ratio_mean': 0.0075, + 'profit_closed_percent_mean': 0.75, + 'profit_closed_ratio_sum': 0.015, + 'profit_closed_percent_sum': 1.5, + 'trade_count': 6, + 'closed_trade_count': 2, + 'winning_trades': 2, 'losing_trades': 0, } From 906c4e64d335e6d401eac2c85d3fa0f0b2319f46 Mon Sep 17 00:00:00 2001 From: Ugur Cem Ozturk Date: Sun, 11 Apr 2021 15:38:08 +0300 Subject: [PATCH 128/460] chore(readme): Fix markdown of docker manual Link to docker-compose was pointing to the one from develop branch. It's changed as with the stable docker-compose. --- docs/docker_quickstart.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index ca0515281..b133e33f0 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -22,10 +22,10 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co ### Docker quick start -Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. +Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -=== "PC/MAC/Linux" - ``` bash +#### PC/MAC/Linux + mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` + + +#### RaspberryPi -=== "RaspberryPi" - ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,7 +56,6 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -68,12 +67,11 @@ Create a new directory and place the [docker-compose file](https://github.com/fr The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -!!! Question "How to edit the bot configuration?" - You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +### Question: "How to edit the bot configuration?" +You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - -#### Adding a custom strategy +##### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -81,7 +79,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a The `SampleStrategy` is run by default. -!!! Warning "`SampleStrategy` is just a demo!" +#### Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -92,7 +90,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -!!! Warning "Default configuration" +#### Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -122,7 +120,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -!!! Warning "Check the Changelog" +#### Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -131,7 +129,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -!!! Note "`docker-compose run --rm`" +#### Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From 1b925ec4a9eff02f47be05263b472ae298c36631 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:11 +0000 Subject: [PATCH 129/460] Bump sqlalchemy from 1.4.5 to 1.4.7 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.5 to 1.4.7. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1877624cf..8bcb9608c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.46.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.5 +SQLAlchemy==1.4.7 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 53bbb2b42c9e808252f8bdc7dd0a45d76c29f4bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:47 +0000 Subject: [PATCH 130/460] Bump ccxt from 1.46.38 to 1.47.47 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.46.38 to 1.47.47. - [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.46.38...1.47.47) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1877624cf..8705ef8e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.46.38 +ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From c19ebc0157cd7157099182e101aed8006ba463fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:58 +0000 Subject: [PATCH 131/460] Bump mkdocs-material from 7.1.0 to 7.1.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.0 to 7.1.1. - [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/7.1.0...7.1.1) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 3dbaea111..cfd63d1d0 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.0 +mkdocs-material==7.1.1 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From f1ac6853fc64270a32930e991d6d809c9576bc55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Apr 2021 11:11:53 +0200 Subject: [PATCH 132/460] Fix discord invite link --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index c890caf1a..7233a92fe 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -156,7 +156,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - 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-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/MA9v74M). 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: From d4dc05980c8b155dd8e7a1673387fdc20f505903 Mon Sep 17 00:00:00 2001 From: Chris van de Steeg Date: Mon, 12 Apr 2021 16:01:46 +0200 Subject: [PATCH 133/460] Update ftx.py Stoploss price should be set as param instead of passing it as price according to ccxt --- freqtrade/exchange/ftx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6312759b9..fdfd5a674 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -63,10 +63,11 @@ class Ftx(Exchange): # set orderPrice to place limit order, otherwise it's a market order params['orderPrice'] = limit_rate + params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', - amount=amount, price=stop_price, params=params) + amount=amount, params=params) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) return order From 1194d0c0f4ca3e6f7c595345849e839b0ff30a43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Apr 2021 20:03:37 +0200 Subject: [PATCH 134/460] Update brew before installing packages --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e15a5a89..102e6ed78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,7 @@ jobs: - name: Installation - macOS run: | + brew update brew install hdf5 c-blosc python -m pip install --upgrade pip export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH From 9a58a8534773393181012bc93930f1b138530c9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:17:11 +0200 Subject: [PATCH 135/460] Don't export "hum" date versions for trade objects. They are not used and have a rather high performance penalty due to using arrow.get --- freqtrade/persistence/models.py | 3 --- freqtrade/rpc/api_server/api_schemas.py | 2 -- freqtrade/rpc/telegram.py | 1 + tests/rpc/test_rpc.py | 4 ---- tests/rpc/test_rpc_apiserver.py | 4 ---- tests/rpc/test_rpc_telegram.py | 2 -- tests/test_persistence.py | 4 ---- 7 files changed, 1 insertion(+), 19 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a22e75e1e..8b4aa325a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -294,15 +294,12 @@ class LocalTrade(): 'fee_close_cost': self.fee_close_cost, 'fee_close_currency': self.fee_close_currency, - 'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), '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_value, 8), - 'close_date_hum': (arrow.get(self.close_date).humanize() - if self.close_date else None), 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index eaca477d7..41de0134c 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -151,13 +151,11 @@ class TradeSchema(BaseModel): fee_close: Optional[float] fee_close_cost: Optional[float] fee_close_currency: Optional[str] - open_date_hum: str open_date: str open_timestamp: int open_rate: float open_rate_requested: Optional[float] open_trade_value: float - close_date_hum: Optional[str] close_date: Optional[str] close_timestamp: Optional[int] close_rate: Optional[float] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 17ddd1c91..a8c629149 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -294,6 +294,7 @@ class Telegram(RPCHandler): messages = [] for r in results: + r['open_date_hum'] = arrow.get(r['open_date']).humanize() lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 64918ed47..a97f6b65e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -53,7 +53,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, @@ -73,7 +72,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, @@ -121,7 +119,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, @@ -141,7 +138,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e72749715..2b6d96c61 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -755,7 +755,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'amount_requested': 123.0, 'base_currency': 'BTC', 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'close_profit': None, 'close_profit_pct': None, @@ -770,7 +769,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'open_order': None, 'open_rate': 0.123, @@ -922,11 +920,9 @@ def test_api_forcebuy(botclient, mocker, fee): 'amount_requested': 1, 'trade_id': 22, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'close_rate': 0.265441, 'open_date': ANY, - 'open_date_hum': 'just now', 'open_timestamp': ANY, 'open_rate': 0.245441, 'pair': 'ETH/ETH', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 27babb1b7..34bf057cb 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -177,9 +177,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': arrow.utcnow(), - 'open_date_hum': arrow.utcnow().humanize, 'close_date': None, - 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3336e4e66..0a3d6858d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -799,11 +799,9 @@ def test_to_json(default_conf, fee): assert result == {'trade_id': None, 'pair': 'ETH/BTC', 'is_open': None, - 'open_date_hum': '2 hours ago', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_order_id': 'dry_run_buy_12345', - 'close_date_hum': None, 'close_date': None, 'close_timestamp': None, 'open_rate': 0.123, @@ -865,10 +863,8 @@ def test_to_json(default_conf, fee): assert result == {'trade_id': None, 'pair': 'XRP/BTC', - 'open_date_hum': '2 hours ago', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date_hum': 'an hour ago', 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, From 4b902d6eb8e0e17e94365bd4de2b6c8c256d8b24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:23:11 +0200 Subject: [PATCH 136/460] Don't use response-model on trades endpoint for now --- freqtrade/rpc/api_server/api_v1.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..663cc9ff4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -17,8 +17,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, - StrategyResponse, TradeResponse, Version, - WhitelistResponse) + StrategyResponse, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -83,7 +82,9 @@ def status(rpc: RPC = Depends(get_rpc)): return [] -@router.get('/trades', response_model=TradeResponse, tags=['info', 'trading']) +# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) +# on big databases. Correct response model: response_model=TradeResponse, +@router.get('/trades', tags=['info', 'trading']) def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) From 9b23be402114cdde6f4ed7d2468c3b90595a931d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:49:53 +0200 Subject: [PATCH 137/460] Return a copy from `current_whitelist` this avoids manipulating of the pair whitelist from within a strategy --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index a035b7c3b..b4dea0743 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -170,6 +170,6 @@ class DataProvider: """ if self._pairlists: - return self._pairlists.whitelist + return self._pairlists.whitelist.copy() else: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") From f1cf56cc4277ec5c9f7184a58f29b9169200d984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:57:21 +0200 Subject: [PATCH 138/460] Update current_whitelist test --- tests/data/test_dataprovider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index ee2e551b6..6b33fa7f2 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -214,8 +214,8 @@ def test_current_whitelist(mocker, default_conf, tickers): pairlist.refresh_pairlist() assert dp.current_whitelist() == pairlist._whitelist - # The identity of the 2 lists should be identical - assert dp.current_whitelist() is pairlist._whitelist + # The identity of the 2 lists should not be identical, but a copy + assert dp.current_whitelist() is not pairlist._whitelist with pytest.raises(OperationalException): dp = DataProvider(default_conf, exchange) From 99e7ee12731533d91b2206e7f2df5008260ffb83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 08:26:26 +0200 Subject: [PATCH 139/460] Fix ftx stoploss creation test --- tests/exchange/test_ftx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 17cfb26fa..494d86e56 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -39,8 +39,9 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE 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'] == 190 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 190 assert api_mock.create_order.call_count == 1 @@ -55,8 +56,8 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE 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 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, @@ -69,9 +70,9 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE 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 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 # test exception handling with pytest.raises(DependencyException): From e0f2bb6160f9478688601a0ffda7b35cb26439ca Mon Sep 17 00:00:00 2001 From: wr0ngc0degen Date: Tue, 13 Apr 2021 11:44:07 +0200 Subject: [PATCH 140/460] update conda dependencies to make compatible with tables package - restrict python version in conda's environment.yml to fixed installation issues due to current incompatibility of tables package with python 3.9 --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 938b5b6b8..f58434c15 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: # - defaults dependencies: # 1/4 req main - - python>=3.7 + - python>=3.7,<3.9 - numpy - pandas - pip From 37c8fd6ad799ed0320147304304acdad3e18e0b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 11:55:03 +0200 Subject: [PATCH 141/460] Remove arrow from models.py --- freqtrade/persistence/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8b4aa325a..49d3e2d62 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,7 +6,6 @@ from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional -import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError @@ -160,8 +159,8 @@ class Order(_DECL_BASE): if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False if order.get('filled', 0) > 0: - self.order_filled_date = arrow.utcnow().datetime - self.order_update_date = arrow.utcnow().datetime + self.order_filled_date = datetime.now(timezone.utc) + self.order_update_date = datetime.now(timezone.utc) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): From 638cd4e8f13b963b03beec0b311f520cf35f0185 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:04:22 +0200 Subject: [PATCH 142/460] Upgrade cleanup action to latest version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102e6ed78..4169661c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.2.2 + uses: rokroskar/workflow-run-cleanup-action@v0.3.2 if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From e4bb6b158294a0b6b36ac5b9be41f93c9bbedafe Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:28:07 +0200 Subject: [PATCH 143/460] Add kucoin exchange subclass Kucoin has some specific orderbook restrictions closes #4723 --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bittrex.py | 4 ---- freqtrade/exchange/exchange.py | 14 +++++++++++--- freqtrade/exchange/kucoin.py | 24 ++++++++++++++++++++++++ tests/exchange/test_ccxt_compat.py | 13 ++++++++++--- tests/exchange/test_exchange.py | 3 +++ 6 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 freqtrade/exchange/kucoin.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 8a5563623..889bb49c2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken +from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index fd7d47668..69e2f2b8d 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -12,10 +12,6 @@ class Bittrex(Exchange): """ Bittrex exchange class. Contains adjustments needed for Freqtrade to work with this exchange. - - Please note that this exchange is not included in the list of exchanges - officially supported by the Freqtrade development team. So some features - may still not work as expected. """ _ft_has: Dict = { diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3224255d0..e52e0e0d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -63,6 +63,7 @@ class Exchange: "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", "l2_limit_range": None, + "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} @@ -1154,14 +1155,20 @@ class Exchange: return self.fetch_order(order_id, pair) @staticmethod - def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]], + range_required: bool = True): """ Get next greater value in the list. Used by fetch_l2_order_book if the api only supports a limited range """ if not limit_range: return limit - return min([x for x in limit_range if limit <= x] + [max(limit_range)]) + + result = min([x for x in limit_range if limit <= x] + [max(limit_range)]) + if not range_required and limit > result: + # Range is not required - we can use None as parameter. + return None + return result @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: @@ -1171,7 +1178,8 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ - limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'], + self._ft_has['l2_limit_range_required']) try: return self._api.fetch_l2_order_book(pair, limit1) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py new file mode 100644 index 000000000..22886a1d8 --- /dev/null +++ b/freqtrade/exchange/kucoin.py @@ -0,0 +1,24 @@ +""" Kucoin exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Kucoin(Exchange): + """ + Kucoin exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "l2_limit_range": [20, 100], + "l2_limit_range_required": False, + } diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 870e6cabd..dce10da84 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -36,7 +36,12 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', - } + }, + 'kucoin': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, } @@ -100,14 +105,16 @@ class TestCCXTExchange(): assert 'asks' in l2 assert 'bids' in l2 l2_limit_range = exchange._ft_has['l2_limit_range'] + l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list(val, l2_limit_range) - if next_limit > 200: + next_limit = exchange.get_next_limit_in_list( + val, l2_limit_range, l2_limit_range_required) + if next_limit is None or next_limit > 200: # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4ceba6eba..db67d038c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1641,6 +1641,9 @@ def test_get_next_limit_in_list(): # Going over the limit ... assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 + # Without required range + assert Exchange.get_next_limit_in_list(2000, limit_range, False) is None + assert Exchange.get_next_limit_in_list(15, limit_range, False) == 20 assert Exchange.get_next_limit_in_list(21, None) == 21 assert Exchange.get_next_limit_in_list(100, None) == 100 From 521e48c94a5e5b18ba409d965757c7b8f509f08f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 13:55:08 +0200 Subject: [PATCH 144/460] Add doc section for Kucoin part of #4723 --- docs/exchanges.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 1c5956088..662f2b908 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -100,6 +100,18 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll } ``` +## Kucoin + +Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: + +```json +"exchange": { + "name": "kucoin", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "your_exchange_api_key_password", +``` + ## All exchanges 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. From 82d66410f74b47f25cb4af60b702cad6fdebcae8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 19:20:57 +0200 Subject: [PATCH 145/460] Fix /performance output if multiple messages are necessary closes #4726 --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a8c629149..09b7b235c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -702,7 +702,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: - self._send_msg(output) + self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line From c2f35ce416cf8d6e6c5806dd1a3e624a124d3843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 20:09:22 +0200 Subject: [PATCH 146/460] /balance should use cached tickers when possible --- freqtrade/exchange/exchange.py | 18 ++++++++++++++++-- freqtrade/rpc/rpc.py | 2 +- tests/exchange/test_exchange.py | 8 ++++++++ tests/rpc/test_rpc.py | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e52e0e0d0..3627a07e4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async +from cachetools import TTLCache from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame @@ -84,6 +85,9 @@ class Exchange: # Timestamp of last markets refresh self._last_markets_refresh: int = 0 + # Cache for 10 minutes ... + self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} @@ -693,9 +697,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_tickers(self) -> Dict: + def get_tickers(self, cached: bool = False) -> Dict: + """ + :param cached: Allow cached result + :return: fetch_tickers result + """ + if cached: + tickers = self._fetch_tickers_cache.get('fetch_tickers') + if tickers: + return tickers try: - return self._api.fetch_tickers() + tickers = self._api.fetch_tickers() + self._fetch_tickers_cache['fetch_tickers'] = tickers + return tickers except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 59758a573..88f8b36db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -442,7 +442,7 @@ class RPC: output = [] total = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers() + tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index db67d038c..76095be2d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1319,6 +1319,14 @@ def test_get_tickers(default_conf, mocker, exchange_name): assert tickers['ETH/BTC']['ask'] == 1 assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 + assert api_mock.fetch_tickers.call_count == 1 + + api_mock.fetch_tickers.reset_mock() + + # Cached ticker should not call api again + tickers2 = exchange.get_tickers(cached=True) + assert tickers2 == tickers + assert api_mock.fetch_tickers.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a97f6b65e..199845545 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -569,6 +569,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) + assert tickers.call_count == 1 + assert tickers.call_args.kwargs['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From c316531c491c995c62bda57bda177669b8d41690 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 21:54:06 +0200 Subject: [PATCH 147/460] make tests 3.7 compatible --- tests/exchange/test_exchange.py | 2 ++ tests/rpc/test_rpc.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 76095be2d..882cf6b5a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1327,6 +1327,8 @@ def test_get_tickers(default_conf, mocker, exchange_name): tickers2 = exchange.get_tickers(cached=True) assert tickers2 == tickers assert api_mock.fetch_tickers.call_count == 0 + tickers2 = exchange.get_tickers(cached=False) + assert api_mock.fetch_tickers.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 199845545..a548505a7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args.kwargs['cached'] is True + assert tickers.call_args[1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From ba38e398e42359293dd13f00b7b44615771f5fd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 22:17:42 +0200 Subject: [PATCH 148/460] Add type hint --- freqtrade/exchange/exchange.py | 2 +- tests/rpc/test_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3627a07e4..3958f6838 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -86,7 +86,7 @@ class Exchange: self._last_markets_refresh: int = 0 # Cache for 10 minutes ... - self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a548505a7..63e09cbe1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args[1]['cached'] is True + assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From 862df2b431021d43f8ab5fcba198cff07250d336 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 19:43:32 +0200 Subject: [PATCH 149/460] Add blacklist recommendation for kucoin closes #4738 --- docs/exchanges.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 662f2b908..8797ade8c 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -7,10 +7,10 @@ This page combines common gotchas and informations which are exchange-specific a !!! Tip "Stoploss on Exchange" Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. -### Blacklists +### Binance Blacklist For Binance, please add `"BNB/"` to your blacklist to avoid issues. -Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. +Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. ### Binance sites @@ -112,6 +112,11 @@ Kucoin requries a passphrase for each api key, you will therefore need to add th "password": "your_exchange_api_key_password", ``` +### Kucoin Blacklists + +For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. +Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore. + ## All exchanges 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. From e820814809f6bef69f1501f16fb797d841034dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:32:34 +0200 Subject: [PATCH 150/460] Default-stoploss-hyperopt should use decimal space, nto real --- freqtrade/optimize/hyperopt_interface.py | 3 ++- freqtrade/optimize/space/__init__.py | 4 ++++ freqtrade/optimize/{ => space}/decimalspace.py | 0 freqtrade/strategy/hyper.py | 2 +- freqtrade/templates/sample_hyperopt_advanced.py | 4 ++-- tests/optimize/test_hyperopt.py | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 freqtrade/optimize/space/__init__.py rename freqtrade/optimize/{ => space}/decimalspace.py (100%) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 633c8bdd5..fa28463e9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict +from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IStrategy @@ -167,7 +168,7 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] def generate_trailing_params(self, params: Dict) -> Dict: diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py new file mode 100644 index 000000000..bbdac4ab9 --- /dev/null +++ b/freqtrade/optimize/space/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa: F401 +from skopt.space import Categorical, Dimension, Integer, Real + +from .decimalspace import SKDecimal diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/space/decimalspace.py similarity index 100% rename from freqtrade/optimize/decimalspace.py rename to freqtrade/optimize/space/decimalspace.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 3fedda974..16b576a73 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,7 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical - from freqtrade.optimize.decimalspace import SKDecimal + from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7736570f7..32ba21716 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Dict, List import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -237,7 +237,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): 'stoploss' optimization hyperspace. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] @staticmethod diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 129fe53d9..59bc4aefb 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,10 +15,10 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException -from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, From 52c482cecfd3133a1e8f741f9b708eec55bd41f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:34:34 +0200 Subject: [PATCH 151/460] Convert trailing and roi defaults to skdecimal --- freqtrade/optimize/hyperopt_interface.py | 15 +++++++++------ freqtrade/templates/sample_hyperopt_advanced.py | 10 +++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index fa28463e9..1bb471b9c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ import math from abc import ABC from typing import Any, Callable, Dict, List -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -155,9 +155,12 @@ class IHyperOpt(ABC): Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'), - Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'), - Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + name='roi_p1'), + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + name='roi_p2'), + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + name='roi_p3'), ] def stoploss_space(self) -> List[Dimension]: @@ -198,14 +201,14 @@ class IHyperOpt(ABC): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 32ba21716..cc13b6ba3 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -223,9 +223,9 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(10, 120, name='roi_t1'), Integer(10, 60, name='roi_t2'), Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), + SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), + SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), + SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), ] @staticmethod @@ -256,14 +256,14 @@ class AdvancedSampleHyperOpt(IHyperOpt): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] From fa343b0484a5850ee9181a7a825ef9f3653d5d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:19:30 +0100 Subject: [PATCH 152/460] Fix get_min_pair_stake_amount formula --- freqtrade/exchange/exchange.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3958f6838..ea6bcb29f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -539,7 +539,9 @@ class Exchange: # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += abs(stoploss) + amount_reserve_percent = ( + amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) From 885096f2b3830e14bf8b1678ad31e696775ddfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:22:52 +0100 Subject: [PATCH 153/460] Update tests for get_min_pair_stake_amount --- tests/exchange/test_exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 882cf6b5a..3bfff50e8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -371,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * 1.1) + assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -383,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * 1.1) + assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -395,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * 1.1) + assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -407,10 +407,10 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * 1.1) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * 1.45) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -432,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) def test_set_sandbox(default_conf, mocker): From ce23d9dfeef32c1a67f0cfbcc28fd39ff4305b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:38:08 +0100 Subject: [PATCH 154/460] Fix test min stake amount --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3bfff50e8..4531bf816 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -410,7 +410,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) + assert isclose(result, max(8, 2 * 2) * 1.5) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) From c9c039d640b59e0794b7c54ff30a1f8412e6c86d Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 15 Apr 2021 15:21:28 +0200 Subject: [PATCH 155/460] remove `copy()` from `custom_info` example `set_index` automatically copies if not stated otherwise with `inplace=True` > inplacebool, default False If True, modifies the DataFrame in place (do not create a new object). from: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.set_index.html?highlight=set_index#pandas.DataFrame.set_index --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7fa824a5b..96c927965 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -57,7 +57,7 @@ class AwesomeStrategy(IStrategy): dataframe['atr'] = ta.ATR(dataframe) if self.dp.runmode.value in ('backtest', 'hyperopt'): # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') return dataframe ``` From 7142787256fd870193699929c377c0706a01599b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 15:41:35 +0200 Subject: [PATCH 156/460] Roll back unintended changes that break rendering --- docs/docker_quickstart.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index b133e33f0..9096000c1 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -14,7 +14,7 @@ To simplify running freqtrade, please install [`docker-compose`](https://docs.do ## Freqtrade with docker-compose -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. !!! Note - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. @@ -24,8 +24,8 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -#### PC/MAC/Linux - +=== "PC/MAC/Linux" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - - -#### RaspberryPi + ``` +=== "RaspberryPi" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,6 +56,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -67,11 +68,12 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -### Question: "How to edit the bot configuration?" -You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. +!!! Question "How to edit the bot configuration?" + You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -##### Adding a custom strategy + You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. + +#### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -79,7 +81,7 @@ You can also change the both Strategy and commands by editing the command sectio The `SampleStrategy` is run by default. -#### Warning "`SampleStrategy` is just a demo!" +!!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -90,7 +92,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -#### Warning "Default configuration" +!!! Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -120,7 +122,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -#### Warning "Check the Changelog" +!!! Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -129,7 +131,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -#### Note "`docker-compose run --rm`" +!!! Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From ce870bbcf7b7b4fe2dfd79884c2da33f225d4af3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 21:38:20 +0200 Subject: [PATCH 157/460] Use 3 decimals for ROI space --- freqtrade/optimize/hyperopt_interface.py | 10 +++++----- freqtrade/optimize/space/decimalspace.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 1bb471b9c..889854cad 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -140,7 +140,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -149,17 +149,17 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=3, name='roi_p1'), - SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=3, name='roi_p2'), - SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=3, name='roi_p3'), ] diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index f5370b6d6..643999cc1 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -9,8 +9,9 @@ class SKDecimal(Integer): self.decimals = decimals _low = int(low * pow(10, self.decimals)) _high = int(high * pow(10, self.decimals)) - self.low_orig = low - self.high_orig = high + # trunc to precision to avoid points out of space + self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals) + self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals) super().__init__(_low, _high, prior, base, transform, name, dtype) From 5e51ba6258c77a33e7b06ed132159ade64029c7d Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:38:00 +0100 Subject: [PATCH 158/460] fix flake8 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6bcb29f..ed7918b36 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -541,7 +541,7 @@ class Exchange: DEFAULT_AMOUNT_RESERVE_PERCENT) amount_reserve_percent = ( amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4531bf816..27f4d0db9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -432,7 +432,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) + assert round(result, 8) == round( + max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), + 8 + ) def test_set_sandbox(default_conf, mocker): From 01b303e0f95bf3c1dc4c7a375b82bcf2f29fc645 Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:58:07 +0100 Subject: [PATCH 159/460] Aligning the doc --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index eb3351b8f..0ade558f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -167,7 +167,7 @@ This exchange has also a limit on USD - where all orders must be > 10$ - which h To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). -With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). +With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take in account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`). To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. From 2011912a19620c337b37a777123e80f86f0c9ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 07:46:00 +0200 Subject: [PATCH 160/460] Adapt documentation to use 3 decimals only --- docs/hyperopt.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 07cc963cf..0e6bded92 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -460,23 +460,26 @@ As stated in the comment, you can also use it as the value of the `minimal_roi` #### Default ROI Search Space -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 3 digits after the decimal point): -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ------------- | -------- | ----------- | ---------- | ------------- | ------------ | ------------- | +| 1 | 0 | 0.011...0.119 | 0 | 0.03...0.31 | 0 | 0.068...0.711 | 0 | 0.121...1.258 | +| 2 | 2...8 | 0.007...0.042 | 10...40 | 0.02...0.11 | 120...480 | 0.045...0.252 | 2880...11520 | 0.081...0.446 | +| 3 | 4...20 | 0.003...0.015 | 20...100 | 0.01...0.04 | 240...1200 | 0.022...0.091 | 5760...28800 | 0.040...0.162 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -516,6 +519,9 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Trailing Stop results If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: @@ -551,6 +557,9 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Reproducible results The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. From 8ce5522a100f2058f7ba20a662fbfba3eb78cf71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 08:00:04 +0200 Subject: [PATCH 161/460] Add additional documentation for SKDecimal space --- docs/advanced-hyperopt.md | 24 +++++++++++++++++++++++- docs/hyperopt.md | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index cc71f39a7..c86978b80 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -79,9 +79,31 @@ class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. def stoploss_space(self): - return [Real(-0.05, -0.01, name='stoploss')] + return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')] ``` +## Space options + +For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: + +* `Categorical` - Pick from a list of categories (e.g. `Categorical(['a', 'b', 'c'], name="cat")`) +* `Integer` - Pick from a range of whole numbers (e.g. `Integer(1, 10, name='rsi')`) +* `SKDecimal` - Pick from a range of decimal numbers with limited precision (e.g. `SKDecimal(0.1, 0.5, decimals=3, name='adx')`). *Available only with freqtrade*. +* `Real` - Pick from a range of decimal numbers with full precision (e.g. `Real(0.1, 0.5, name='adx')` + +You can import all of these from `freqtrade.optimize.space`, although `Categorical`, `Integer` and `Real` are only aliases for their corresponding scikit-optimize Spaces. `SKDecimal` is provided by freqtrade for faster optimizations. + +``` python +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa +``` + +!!! Hint "SKDecimal vs. Real" + We recommend to use `SKDecimal` instead of the `Real` space in almost all cases. While the Real space provides full accuracy (up to ~16 decimal places) - this precision is rarely needed, and leads to unnecessary long hyperopt times. + + Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`). + + A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`). + --- ## Legacy Hyperopt diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0e6bded92..b073a73b6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -294,6 +294,7 @@ Based on the results, hyperopt will tell you which parameter combination produce ## Parameter types There are four parameter types each suited for different purposes. + * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. * `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. From e6936ae1352246125a66685abf9ac35d05fbce3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:16:29 +0200 Subject: [PATCH 162/460] Improve wording in docs --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b073a73b6..21cbadf7f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -479,7 +479,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Stoploss results @@ -521,7 +521,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Trailing Stop results @@ -559,7 +559,7 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Reproducible results From aeb81f90ff0cf375def82f21194c922baa4c4b76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:35:56 +0200 Subject: [PATCH 163/460] Implement errorhandling for /trade endpoint --- freqtrade/rpc/api_server/api_v1.py | 5 ++++- tests/rpc/test_rpc_apiserver.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6873c0c4c..02736aca6 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -90,7 +90,10 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_status([tradeid])[0] + try: + return rpc._rpc_trade_status([tradeid])[0] + except (RPCException, KeyError): + raise HTTPException(status_code=404, detail='Trade not found.') @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..760d78b03 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -522,6 +522,26 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 1 +def test_api_trade_single(botclient, mocker, fee, ticker, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + fetch_ticker=ticker, + ) + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc, 404) + assert rc.json()['detail'] == 'Trade not found.' + + create_mock_trades(fee) + Trade.query.session.flush() + + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc) + assert rc.json()['trade_id'] == 3 + + def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 5c579613e1cf8505f659210ecae518168a0f026b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:42:13 +0200 Subject: [PATCH 164/460] add /trade endpoint to rest_client script --- docs/rest-api.md | 12 ++++++++---- scripts/rest_client.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index be3107fcb..5c25e9eeb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -235,6 +235,9 @@ pair_history performance Return the performance of the different coins. +ping + simple ping + plot_config Return plot configuration if the strategy defines one. @@ -271,15 +274,16 @@ strategy :param strategy: Strategy class name +trade + Return specific trade + + :param trade_id: Specify which trade to get. + trades Return trades history. :param limit: Limits trades to the X last trades. No limit to get all the trades. -trade - Return specific trade. - :param tradeid: Specify which trade to get. - version Return the version of the bot. diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4d667879d..40b338ce8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -127,7 +127,7 @@ class FtRestClient(): return self._delete("locks/{}".format(lock_id)) def daily(self, days=None): - """Return the amount of open trades. + """Return the profits for each day, and amount of trades. :return: json object """ @@ -195,7 +195,7 @@ class FtRestClient(): def logs(self, limit=None): """Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. :return: json object """ return self._get("logs", params={"limit": limit} if limit else 0) @@ -208,6 +208,14 @@ class FtRestClient(): """ return self._get("trades", params={"limit": limit} if limit else 0) + def trade(self, trade_id): + """Return specific trade + + :param trade_id: Specify which trade to get. + :return: json object + """ + return self._get("trade/{}".format(trade_id)) + def delete_trade(self, trade_id): """Delete trade from the database. Tries to close open orders. Requires manual handling of this asset on the exchange. From 1eb9ce4227a0424b5153e8f47d1522fe2ac1de29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:47:32 +0200 Subject: [PATCH 165/460] Allow specifying pairs for optimize commands via `--pairs` --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/configuration/configuration.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9468a7f7d..9cf9992ce 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -17,7 +17,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", - "max_open_trades", "stake_amount", "fee"] + "max_open_trades", "stake_amount", "fee", "pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "enable_protections", "dry_run_wallet", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4fac8ac72..e49895de4 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -330,7 +330,7 @@ AVAILABLE_CLI_OPTIONS = { # Script options "pairs": Arg( '-p', '--pairs', - help='Show profits for only these pairs. Pairs are space-separated.', + help='Limit command to these pairs. Pairs are space-separated.', nargs='+', ), # Download data diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 9acd532cc..cc11f97c2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -445,6 +445,7 @@ class Configuration: """ if "pairs" in config: + config['exchange']['pair_whitelist'] = config['pairs'] return if "pairs_file" in self.args and self.args["pairs_file"]: From 6a9c47d15f7476f72dfb220e643a8801bcf5d058 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:48:24 +0200 Subject: [PATCH 166/460] Update docs with new options --- docs/backtesting.md | 6 +++++- docs/edge.md | 10 +++++++++- docs/hyperopt.md | 11 ++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e16225f94..ee9926f32 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -15,7 +15,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -37,6 +38,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). diff --git a/docs/edge.md b/docs/edge.md index 7f0a9cb2d..237ff36f6 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -215,8 +215,10 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. usage: freqtrade edge [-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] [--stoplosses STOPLOSS_RANGE] + [--fee FLOAT] [-p PAIRS [PAIRS ...]] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -224,6 +226,9 @@ optional arguments: Specify timeframe (`1m`, `5m`, `30m`, `1h`, `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. @@ -232,6 +237,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --stoplosses STOPLOSS_RANGE Defines a range of stoploss values against which edge will assess the strategy. The format is "min,max,step" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 21cbadf7f..51905e616 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,8 +44,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt NAME] + [--hyperopt-path PATH] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -69,6 +70,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --hyperopt NAME Specify hyperopt class name which will be used by the bot. --hyperopt-path PATH Specify additional lookup path for Hyperopt and @@ -105,7 +109,8 @@ optional arguments: reproducible hyperopt results. --min-trades INT Set minimal desired number of trades for evaluations in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function + --hyperopt-loss NAME, --hyperoptloss NAME + Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in From c8d3d449a3b0fee283999d5782939dc0bbc7ff5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:51:02 +0200 Subject: [PATCH 167/460] Add quick test for pair_whitelist overwrite --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a512bf58a..b2c883108 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1002,6 +1002,7 @@ def test_pairlist_resolving(): config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['pair_whitelist'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' From fbb90755395e205f2235229dc2eb14c25ee88c99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:53:03 +0200 Subject: [PATCH 168/460] Update util command structures too --- docs/data-download.md | 6 +++--- docs/plotting.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 04f444a8b..7a78334d5 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -30,7 +30,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. @@ -48,10 +48,10 @@ optional arguments: exchange/pairs/timeframes. --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. - (default: `json`). + (default: `None`). --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: - `jsongz`). + `None`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/plotting.md b/docs/plotting.md index 63afa16b6..5d454c414 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -37,7 +37,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --indicators1 INDICATORS1 [INDICATORS1 ...] Set indicators from your strategy you want in the @@ -90,6 +90,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` Example: @@ -244,7 +245,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --timerange TIMERANGE Specify what timerange of data to use. @@ -286,6 +287,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation. From 44bfb53668d8a5fd301e0c2187623a6e958589a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:29:34 +0200 Subject: [PATCH 169/460] Don't use current rate for closed trades --- freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 11 +++++++---- tests/rpc/test_rpc_apiserver.py | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 41de0134c..12bee1cf2 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -189,7 +189,6 @@ class OpenTradeSchema(TradeSchema): stoploss_current_dist_ratio: Optional[float] stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] - base_currency: str current_profit: float current_profit_abs: float current_profit_pct: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 88f8b36db..b86562e80 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -167,10 +167,13 @@ class RPC: if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user - try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except (ExchangeError, PricingError): - current_rate = NAN + if trade.is_open: + try: + current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + except (ExchangeError, PricingError): + current_rate = NAN + else: + current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3d27922d8..d610906d5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -773,7 +773,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert rc.json()[0] == { 'amount': 123.0, 'amount_requested': 123.0, - 'base_currency': 'BTC', 'close_date': None, 'close_timestamp': None, 'close_profit': None, From 0737e3fa2230ba27dee588476fa589691123554e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:48:29 +0200 Subject: [PATCH 170/460] Clarify refresh_period section for volumepairlist part of #4689 --- docs/includes/pairlists.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8688494cc..85d157e75 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -60,6 +60,8 @@ When used in the chain of Pairlist Handlers in a non-leading position (after Sta When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). +The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. +Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data. `VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: @@ -90,6 +92,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter Sorts pairs by past trade performance, as follows: + 1. Positive performance. 2. No closed trades yet. 3. Negative performance. From 296ea30cc341345bd87a46f99bbe99e511481ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:35 +0000 Subject: [PATCH 171/460] Bump pytest-asyncio from 0.14.0 to 0.15.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.14.0...v0.15.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 cd93f2433..6ddbffb74 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 pytest==6.2.3 -pytest-asyncio==0.14.0 +pytest-asyncio==0.15.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 From 8d2e6954a1fabb833b6e75a5609a4993aeac90bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:41 +0000 Subject: [PATCH 172/460] Bump flake8 from 3.9.0 to 3.9.1 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.0 to 3.9.1. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.0...3.9.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cd93f2433..35c57769a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.9.0 +flake8==3.9.1 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 From 05246e6637637590909e9e34d0a3a813cc14aa5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:52 +0000 Subject: [PATCH 173/460] Bump pandas from 1.2.3 to 1.2.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.3...v1.2.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 129bb05b2..c6051463c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.2 -pandas==1.2.3 +pandas==1.2.4 ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels From 59d02f3f039f4e696ac78f7b2eeb3d7888020224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:27 +0000 Subject: [PATCH 174/460] Bump sqlalchemy from 1.4.7 to 1.4.9 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.7 to 1.4.9. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..5159d69e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.7 +SQLAlchemy==1.4.9 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From b94de3030a11610d48aae70f9e9f182e098c0096 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:33 +0000 Subject: [PATCH 175/460] Bump mkdocs-material from 7.1.1 to 7.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.1 to 7.1.2. - [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/7.1.1...7.1.2) 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 cfd63d1d0..4d7082a7f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.1 +mkdocs-material==7.1.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 9407dbcf87eea8a511642045b2910f54de448f49 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 07:46:39 +0200 Subject: [PATCH 176/460] Add freqtrade powered by ccxt --- README.md | 2 +- docs/assets/ccxt-logo.svg | 3 ++ docs/assets/freqtrade_poweredby.svg | 44 +++++++++++++++++++++++++++++ docs/index.md | 5 ++-- 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 docs/assets/ccxt-logo.svg create mode 100644 docs/assets/freqtrade_poweredby.svg diff --git a/README.md b/README.md index c3a665c47..916f9cf17 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Freqtrade +# ![freqtrade](docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) diff --git a/docs/assets/ccxt-logo.svg b/docs/assets/ccxt-logo.svg new file mode 100644 index 000000000..e52682546 --- /dev/null +++ b/docs/assets/ccxt-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg new file mode 100644 index 000000000..1041f87ab --- /dev/null +++ b/docs/assets/freqtrade_poweredby.svg @@ -0,0 +1,44 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + Freqtrade + + + + + poweredby + + diff --git a/docs/index.md b/docs/index.md index 61f2276c3..c2b6d5629 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ -# Freqtrade +![freqtrade](assets/freqtrade_poweredby.svg) + [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) @@ -39,7 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Kraken](https://kraken.com/) -- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +- [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested From 66b3ecfeed6023794d9710dde825f49d46ed0bd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 08:32:17 +0200 Subject: [PATCH 177/460] Remove faulty font-family in svg --- docs/assets/freqtrade_poweredby.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 1041f87ab..71d165cbf 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,7 +34,7 @@ - Freqtrade + Freqtrade From 0ddc68b37d82bb7ee64ce0851cd050bce0b3539f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 09:41:49 +0000 Subject: [PATCH 178/460] Bump ccxt from 1.47.47 to 1.48.22 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.47.47 to 1.48.22. - [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.47.47...1.48.22) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d71cd1c05..a89eb2383 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.47.47 +ccxt==1.48.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From a2acb54e7efc90fa2ce8af85284518cb840d26fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 15:15:40 +0200 Subject: [PATCH 179/460] Clarify comments in pairlist --- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlist/StaticPairList.py | 2 +- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 184feff9e..c4a9c3e40 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -85,7 +85,7 @@ class IPairList(LoggingMixin, ABC): position in the chain. :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ raise OperationalException("This Pairlist Handler should not be used " diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index c5ced48c9..13d30fc47 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -46,7 +46,7 @@ class StaticPairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ if self._allow_inactive: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index dd8fc64fd..e85fb1805 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -67,7 +67,7 @@ class VolumePairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ # Generate dynamic whitelist From 75612496d7294ca6b0ccd89a62cb16562109d307 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:01:39 +0200 Subject: [PATCH 180/460] Improve poweredBy logo spacing --- docs/assets/freqtrade_poweredby.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 71d165cbf..957ec6401 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,11 +34,11 @@ - Freqtrade + Freqtrade - poweredby + poweredby From c9e901cf325756707e51e4dbda2f060b2fed827b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:31:37 +0200 Subject: [PATCH 181/460] Move wallet tasks to test_wallets --- tests/test_freqtradebot.py | 59 -------------------------------------- tests/test_wallets.py | 59 +++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c91015766..0634df9e4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -207,65 +207,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b freqtrade.get_free_open_trades()) -def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - - -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), - ]) -def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), - get_fee=fee - ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 - conf['max_open_trades'] = 2 - conf['tradable_balance_ratio'] = balance_ratio - - freqtrade = FreqtradeBot(conf) - patch_get_signal(freqtrade) - - # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create 2 trades, order amount should be None - freqtrade.execute_buy('LTC/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - # set max_open_trades = None, so do not trade - conf['max_open_trades'] = 0 - freqtrade = FreqtradeBot(conf) - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - def test_edge_called_in_process(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_edge(mocker) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index b7aead0c4..e6e41bab1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,12 @@ # pragma pylint: disable=missing-docstring +from copy import deepcopy from unittest.mock import MagicMock -from tests.conftest import get_patched_freqtradebot +import pytest + +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException +from tests.conftest import get_patched_freqtradebot, patch_wallet def test_sync_wallet_at_boot(mocker, default_conf): @@ -106,3 +111,55 @@ def test_sync_wallet_missing_data(mocker, default_conf): assert freqtrade.wallets._wallets['GAS'].used is None assert freqtrade.wallets._wallets['GAS'].total == 0.260739 assert freqtrade.wallets.get_free('GAS') == 0.260739 + + +def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: + patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + + +@pytest.mark.parametrize("balance_ratio,result1", [ + (1, 0.005), + (0.99, 0.00495), + (0.50, 0.0025), +]) +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, + limit_buy_order_open, fee, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + buy=MagicMock(return_value=limit_buy_order_open), + get_fee=fee + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT + conf['dry_run_wallet'] = 0.01 + conf['max_open_trades'] = 2 + conf['tradable_balance_ratio'] = balance_ratio + + freqtrade = get_patched_freqtradebot(mocker, conf) + + # no open trades, order amount should be 'balance / max_open_trades' + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' + freqtrade.execute_buy('ETH/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create 2 trades, order amount should be None + freqtrade.execute_buy('LTC/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + assert result == 0 + + # set max_open_trades = None, so do not trade + freqtrade.config['max_open_trades'] = 0 + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + assert result == 0 From bd7e535e42ccca777697f497d0e9264698c4421a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:58:47 +0200 Subject: [PATCH 182/460] Use "human" amounts in stake_amount tests --- tests/test_wallets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index e6e41bab1..562957790 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -122,9 +122,9 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), + (1, 50), + (0.99, 49.5), + (0.50, 25), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, limit_buy_order_open, fee, mocker) -> None: @@ -137,29 +137,29 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r conf = deepcopy(default_conf) conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 + conf['dry_run_wallet'] = 100 conf['max_open_trades'] = 2 conf['tradable_balance_ratio'] = balance_ratio freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) + freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) assert result == 0 From 2254f65fa7f8c32d2d23d559fa57f297bf424dd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 12:54:22 +0200 Subject: [PATCH 183/460] use binance intests instead of bittrex --- tests/commands/test_commands.py | 4 +- tests/config_test_comments.json | 2 +- tests/conftest.py | 8 ++-- tests/conftest_trades.py | 12 ++--- tests/plugins/test_protections.py | 2 +- tests/rpc/test_rpc.py | 4 +- tests/rpc/test_rpc_apiserver.py | 8 ++-- tests/rpc/test_rpc_manager.py | 2 +- tests/rpc/test_rpc_telegram.py | 22 ++++----- tests/rpc/test_rpc_webhook.py | 10 ++-- tests/strategy/test_interface.py | 8 ++-- tests/test_freqtradebot.py | 77 ++++++++++++++++++++----------- tests/test_persistence.py | 54 +++++++++++----------- 13 files changed, 117 insertions(+), 96 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 232fc4e2c..d86bced5d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -116,7 +116,7 @@ def test_list_timeframes(mocker, capsys): '1h': 'hour', '1d': 'day', } - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') args = [ "list-timeframes", ] @@ -201,7 +201,7 @@ def test_list_markets(mocker, markets, capsys): api_mock = MagicMock() api_mock.markets = markets - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') # Test with no --config args = [ diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 4f201f86c..48a087dec 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -59,7 +59,7 @@ } }, "exchange": { - "name": "bittrex", + "name": "binance", "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", diff --git a/tests/conftest.py b/tests/conftest.py index 4a2106a4d..cc4fe91f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,7 +79,7 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: +def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -98,7 +98,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', +def get_patched_exchange(mocker, config, api_mock=None, id='binance', mock_markets=True) -> Exchange: patch_exchange(mocker, api_mock, id, mock_markets) config['exchange']['name'] = id @@ -293,7 +293,7 @@ def get_default_conf(testdatadir): "order_book_max": 1 }, "exchange": { - "name": "bittrex", + "name": "binance", "enabled": True, "key": "key", "secret": "secret", @@ -1765,7 +1765,7 @@ def open_trade(): return Trade( pair='ETH/BTC', open_rate=0.00001099, - exchange='bittrex', + exchange='binance', open_order_id='123456789', amount=90.99181073, fee_open=0.0, diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 34fc58aee..b92b51144 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -31,7 +31,7 @@ def mock_trade_1(fee): is_open=True, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -84,7 +84,7 @@ def mock_trade_2(fee): close_rate=0.128, close_profit=0.005, close_profit_abs=0.000584127, - exchange='bittrex', + exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', @@ -144,7 +144,7 @@ def mock_trade_3(fee): close_rate=0.06, close_profit=0.01, close_profit_abs=0.000155, - exchange='bittrex', + exchange='binance', is_open=False, strategy='DefaultStrategy', timeframe=5, @@ -187,7 +187,7 @@ def mock_trade_4(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -239,7 +239,7 @@ def mock_trade_5(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', stoploss_order_id='prod_stoploss_3455', timeframe=5, @@ -293,7 +293,7 @@ def mock_trade_6(fee): fee_close=fee.return_value, is_open=True, open_rate=0.15, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', open_order_id="prod_sell_6", timeframe=5, diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 545387eaa..a39301145 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -27,7 +27,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, - exchange='bittrex', + exchange='binance', ) trade.recalc_open_trade_value() if not is_open: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 63e09cbe1..6d31e7635 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -106,7 +106,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -172,7 +172,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d610906d5..6505629eb 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -468,7 +468,7 @@ def test_api_show_config(botclient, mocker): rc = client_get(client, f"{BASE_URI}/show_config") assert_response(rc) assert 'dry_run' in rc.json() - assert rc.json()['exchange'] == 'bittrex' + assert rc.json()['exchange'] == 'binance' assert rc.json()['timeframe'] == '5m' assert rc.json()['timeframe_ms'] == 300000 assert rc.json()['timeframe_min'] == 5 @@ -825,7 +825,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -916,7 +916,7 @@ def test_api_forcebuy(botclient, mocker, fee): pair='ETH/ETH', amount=1, amount_requested=1, - exchange='bittrex', + exchange='binance', stake_amount=1, open_rate=0.245441, open_order_id="123456", @@ -979,7 +979,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 3068e9764..6996c932b 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -140,7 +140,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: 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'] + assert "*Exchange:* `binance`" in telegram_mock.call_args_list[1][0][0]['status'] telegram_mock.reset_mock() default_conf['dry_run'] = True diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 34bf057cb..ba32dc385 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -688,7 +688,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.173e-05, @@ -749,7 +749,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.043e-05, @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.099e-05, @@ -1178,7 +1178,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1187,7 +1187,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1197,7 +1197,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1213,7 +1213,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \ + == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1242,11 +1242,11 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) - assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' + assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* ' 'Cancelling open buy Order for ETH/BTC (#1). ' 'Reason: cancelled due to timeout.') @@ -1393,7 +1393,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1405,7 +1405,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 5361cd947..62818ecbb 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -59,7 +59,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -80,7 +80,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -101,7 +101,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -127,7 +127,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -184,7 +184,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'order_type': 'limit', diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 0ee80e0c5..78fa368e4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -219,7 +219,7 @@ def test_min_roi_reached(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -258,7 +258,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -293,7 +293,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -346,7 +346,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) trade.adjust_min_max_rates(trade.open_rate) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0634df9e4..433cce170 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -362,7 +362,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -621,7 +621,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.stake_amount == default_conf['stake_amount'] assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert trade.open_rate == 0.00001098 assert trade.amount == 91.07468123 @@ -718,7 +718,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', )) Trade.query.session.add(Trade( pair='ETH/BTC', @@ -728,7 +728,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=12, open_rate=0.001, - exchange='bittrex', + exchange='binance', )) assert pair not in freqtrade.active_pair_whitelist @@ -969,7 +969,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None return_value=limit_buy_order['amount']) stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1001,6 +1001,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss ) freqtrade = FreqtradeBot(default_conf) @@ -1025,7 +1028,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1038,7 +1041,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1064,14 +1067,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False mocker.patch( - 'freqtrade.exchange.Exchange.stoploss', + 'freqtrade.exchange.Binance.stoploss', side_effect=ExchangeError() ) trade.is_open = True @@ -1083,9 +1086,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1095,7 +1098,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.is_open = False stoploss.reset_mock() mocker.patch('freqtrade.exchange.Exchange.fetch_order') - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1115,6 +1118,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), stoploss=MagicMock(side_effect=ExchangeError()), ) @@ -1149,6 +1155,9 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value=limit_buy_order_open), sell=sell_mock, get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) @@ -1194,6 +1203,9 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, sell=sell_mock, get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=MagicMock(side_effect=InsufficientFundsError()), ) patch_get_signal(freqtrade) @@ -1231,6 +1243,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1271,7 +1286,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1286,8 +1301,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1334,6 +1349,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1369,9 +1387,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1380,8 +1398,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) + cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) + mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1403,6 +1421,9 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1443,7 +1464,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1457,8 +1478,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -2603,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert { 'trade_id': 1, 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -2653,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.044e-05, @@ -2710,7 +2731,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.08801e-05, @@ -2916,7 +2937,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -3899,7 +3920,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert len(Trade.query.all()) == 1 @@ -4355,7 +4376,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', ) Trade.query.session.add(trade) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0a3d6858d..3b90f368f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -64,7 +64,7 @@ def test_init_dryrun_db(default_conf, tmpdir): @pytest.mark.usefixtures("init_persistence") -def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): +def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ On this test we will buy and sell a crypto currency. @@ -102,7 +102,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None assert trade.close_profit is None @@ -142,7 +142,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().datetime, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -177,7 +177,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -205,7 +205,7 @@ def test_trade_close(limit_buy_order, limit_sell_order, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, - exchange='bittrex', + exchange='binance', ) assert trade.close_profit is None assert trade.close_date is None @@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -250,7 +250,7 @@ def test_update_open_order(limit_buy_order): amount=5, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None @@ -274,7 +274,7 @@ def test_update_invalid_order(limit_buy_order): open_rate=0.001, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -290,7 +290,7 @@ def test_calc_open_trade_value(limit_buy_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'open_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -311,7 +311,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'close_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -336,7 +336,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -370,7 +370,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -400,7 +400,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) Trade.query.session.add(trade) @@ -412,7 +412,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_sell_12345' ) Trade.query.session.add(trade) @@ -425,7 +425,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345' ) Trade.query.session.add(trade) @@ -463,7 +463,7 @@ def test_migrate_old(mocker, default_conf, fee): );""" insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee, open_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee}, + VALUES ('binance', 'BTC_ETC', 1, '123123', {fee}, 0.00258580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -472,7 +472,7 @@ def test_migrate_old(mocker, default_conf, fee): ) insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee, open_rate, close_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 0, {fee}, + VALUES ('binance', 'BTC_ETC', 0, {fee}, 0.00258580, 0.00268580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -500,7 +500,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.amount_requested == amount assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" - assert trade.exchange == "bittrex" + assert trade.exchange == "binance" assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 @@ -694,7 +694,7 @@ def test_adjust_stop_loss(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -746,7 +746,7 @@ def test_adjust_min_max_rates(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -790,7 +790,7 @@ def test_to_json(default_conf, fee): fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) result = trade.to_json() @@ -841,7 +841,7 @@ def test_to_json(default_conf, fee): 'max_rate': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } # Simulate dry_run entries @@ -856,7 +856,7 @@ def test_to_json(default_conf, fee): close_date=arrow.utcnow().shift(hours=-1).datetime, open_rate=0.123, close_rate=0.125, - exchange='bittrex', + exchange='binance', ) result = trade.to_json() assert isinstance(result, dict) @@ -906,7 +906,7 @@ def test_to_json(default_conf, fee): 'sell_order_status': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } @@ -919,7 +919,7 @@ def test_stoploss_reinitialization(default_conf, fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -978,7 +978,7 @@ def test_update_fee(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -1017,7 +1017,7 @@ def test_fee_updated(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) From 1936dd1ee8a0c3187dfec1216909c71572ae1205 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:45:07 +0200 Subject: [PATCH 184/460] Add test-case verifying "changing" wallet with unlimited amount --- tests/test_wallets.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 562957790..86f49698b 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -121,13 +121,14 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 50), - (0.99, 49.5), - (0.50, 25), +@pytest.mark.parametrize("balance_ratio,result1,result2", [ + (1, 50, 66.66666), + (0.99, 49.5, 66.0), + (0.50, 25, 33.3333), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: + result2, limit_buy_order_open, + fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -150,7 +151,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None @@ -159,6 +160,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 + freqtrade.config['max_open_trades'] = 3 + freqtrade.config['dry_run_wallet'] = 200 + freqtrade.wallets.start_cap = 200 + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + assert round(result, 4) == round(result2, 4) + # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) From 06d6f9ac41b1425a19062074ba6a0f945a98788f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:55:48 +0200 Subject: [PATCH 185/460] Fix calculation of unlimited_stake in case of modified wallet --- freqtrade/wallets.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f4432e932..889fe6fa8 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -130,14 +130,13 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets - def _get_available_stake_amount(self) -> float: + def _get_available_stake_amount(self, val_tied_up: float) -> float: """ Return the total currently available balance in stake currency, respecting tradable_balance_ratio. Calculated as - ( + free amount ) * tradable_balance_ratio - + ( + free amount) * tradable_balance_ratio - """ - val_tied_up = Trade.total_open_trades_stakes() # Ensure % is used from the overall balance # Otherwise we'd risk lowering stakes with each open trade. @@ -151,12 +150,13 @@ class Wallets: Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades: + if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) - return available_amount / free_open_trades + return (available_amount + val_tied_up) / self._config['max_open_trades'] def _check_available_stake_amount(self, stake_amount: float) -> float: """ @@ -165,7 +165,8 @@ class Wallets: :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio From 71b017e7c34c837b13e039740154cd0896d7bf79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:53:16 +0200 Subject: [PATCH 186/460] Simplify webhook test --- tests/rpc/test_rpc_webhook.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 62818ecbb..bfb9cbb01 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -25,6 +25,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhooksell": { "value1": "Selling {pair}", "value2": "limit {limit:8f}", @@ -35,6 +40,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -76,8 +86,8 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) # Test buy cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() + msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Binance', @@ -97,8 +107,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) # Test sell - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -123,8 +132,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) # Test sell cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'exchange': 'Binance', From fecd5c582b81e191b82a5d90834b0592eb1ffbe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:58:29 +0200 Subject: [PATCH 187/460] Add buy and sell fill notifications closes #3542 --- config_full.json.example | 2 ++ docs/webhook-config.md | 46 +++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 28 ++++++++++++++++--- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 8 ++++++ freqtrade/rpc/webhook.py | 4 +++ tests/rpc/test_rpc_webhook.py | 51 +++++++++++++++++++++++++++++++++-- 7 files changed, 136 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 717797933..973afe2c8 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -163,7 +163,9 @@ "warning": "on", "startup": "on", "buy": "on", + "buy_fill": "on", "sell": "on", + "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" } diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2e41ad2cc..8ce6edc18 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT). "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "" }, "webhooksell": { "value1": "Selling {pair}", @@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT). "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}.", + "value3": "" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -91,6 +101,21 @@ Possible parameters are: * `order_type` * `current_rate` +### Webhookbuyfill + +The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `open_rate` +* `amount` +* `open_date` +* `stake_amount` +* `stake_currency` +* `fiat_currency` + ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. @@ -103,6 +128,27 @@ Possible parameters are: * `limit` * `amount` * `open_rate` +* `profit_amount` +* `profit_ratio` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` +* `open_date` +* `close_date` + +### Webhooksellfill + +The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `gain` +* `close_rate` +* `amount` +* `open_rate` * `current_rate` * `profit_amount` * `profit_ratio` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..68f98ec21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -675,6 +675,21 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def _notify_buy_fill(self, trade: Trade) -> None: + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'open_rate': trade.open_rate, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + } + self.rpc.send_msg(msg) + # # SELL / exit positions / close trades logic and methods # @@ -1212,19 +1227,20 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_sell(self, trade: Trade, order_type: str) -> None: + def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occured. """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.get_sell_rate(trade.pair, False) + current_rate = self.get_sell_rate(trade.pair, False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill + else RPCMessageType.SELL_NOTIFICATION), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1233,6 +1249,7 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'amount': trade.amount, 'open_rate': trade.open_rate, + 'close_rate': trade.close_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, @@ -1344,9 +1361,14 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() + elif trade.open_order_id is None: + # Buy fill + self._notify_buy_fill(trade) + return False def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..bf0b88f6c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -35,8 +35,10 @@ class RPCMessageType(Enum): WARNING_NOTIFICATION = 'warning' STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' + BUY_FILL_NOTIFICATION = 'buy_fill' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' + SELL_FILL_NOTIFICATION = 'sell_fill' SELL_CANCEL_NOTIFICATION = 'sell_cancel' def __repr__(self): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 09b7b235c..4dceeb46c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -209,6 +209,10 @@ class Telegram(RPCHandler): "Cancelling open buy Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) @@ -240,6 +244,10 @@ class Telegram(RPCHandler): message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 5a30a9be8..c7e012af5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -49,8 +49,12 @@ class Webhook(RPCHandler): valuedict = self._config['webhook'].get('webhookbuy', None) elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhookbuycancel', None) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookbuyfill', None) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksellfill', None) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index bfb9cbb01..38d2fe539 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -43,7 +43,7 @@ def get_webhook_dict() -> dict: "webhooksellfill": { "value1": "Sell Order for {pair} filled", "value2": "at {close_rate:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "" }, "webhookstatus": { "value1": "Status: {status}", @@ -59,7 +59,7 @@ def test__init__(mocker, default_conf): assert webhook._config == default_conf -def test_send_msg(default_conf, mocker): +def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -106,6 +106,27 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + # Test buy fill + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'open_rate': 0.005, + 'stake_amount': 0.8, + 'stake_amount_fiat': 500, + 'stake_currency': 'BTC', + 'fiat_currency': 'EUR' + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) # Test sell msg_mock.reset_mock() msg = { @@ -156,6 +177,32 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) + # Test Sell fill + msg_mock.reset_mock() + msg = { + 'type': RPCMessageType.SELL_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': "profit", + 'close_rate': 0.005, + 'amount': 0.8, + 'order_type': 'limit', + 'open_rate': 0.004, + 'current_rate': 0.005, + 'profit_amount': 0.001, + 'profit_ratio': 0.20, + 'stake_currency': 'BTC', + 'sell_reason': SellType.STOP_LOSS.value + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) + for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.STARTUP_NOTIFICATION]: From 8800a097700077955bc5149d567c1df92e1f9445 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 21:32:04 +0200 Subject: [PATCH 188/460] Don't send double-notifications for stoploss fills --- freqtrade/freqtradebot.py | 3 ++- tests/rpc/test_rpc_telegram.py | 13 ++++++++----- tests/test_freqtradebot.py | 23 +++++++++++++++-------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68f98ec21..76212bf97 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,7 +1361,8 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - self._notify_sell(trade, '', True) + if not stoploss_order: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ba32dc385..a3c823aac 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -683,7 +683,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -703,6 +703,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -743,7 +744,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { @@ -764,6 +765,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -794,9 +796,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for each trade 3 times - assert msg_mock.call_count == 8 - msg = msg_mock.call_args_list[1][0][0] + # Called for each trade 4 times + assert msg_mock.call_count == 12 + msg = msg_mock.call_args_list[2][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -815,6 +817,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == msg diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..39c3f0561 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1710,6 +1710,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_rate=0.01, open_date=arrow.utcnow().datetime, amount=11, + exchange="binance", ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) @@ -2262,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 @@ -2319,7 +2320,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # note this is for a partially-complete buy order freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 @@ -2354,7 +2355,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap assert log_has_re(r"Applying fee on amount for Trade.*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2394,7 +2395,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert log_has_re(r"Could not update trade amount: .*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2639,6 +2640,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2689,6 +2691,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2746,7 +2749,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, - + 'close_rate': ANY, } == last_msg @@ -2830,7 +2833,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke trade = Trade.query.first() assert trade assert cancel_order.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, @@ -2898,7 +2901,10 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.stoploss_order_id is None assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2932,7 +2938,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert not trade.is_open assert trade.close_profit == 0.0620716 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -2952,6 +2958,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg From 0341ac5a55bc0a985776c38137489bc81960e80a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:41:58 +0200 Subject: [PATCH 189/460] rename RPC message types --- freqtrade/freqtradebot.py | 16 ++--- freqtrade/rpc/rpc.py | 18 ++--- freqtrade/rpc/rpc_manager.py | 8 +-- freqtrade/rpc/telegram.py | 117 +++++++++++++++++---------------- freqtrade/rpc/webhook.py | 18 ++--- tests/rpc/test_rpc_manager.py | 6 +- tests/rpc/test_rpc_telegram.py | 28 ++++---- tests/rpc/test_rpc_webhook.py | 28 ++++---- tests/test_freqtradebot.py | 14 ++-- 9 files changed, 128 insertions(+), 125 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 76212bf97..c48ea851e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -113,7 +113,7 @@ class FreqtradeBot(LoggingMixin): via RPC about changes in the bot status. """ self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': msg }) @@ -205,7 +205,7 @@ class FreqtradeBot(LoggingMixin): if len(open_trades) != 0: msg = { - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': f"{len(open_trades)} open trades active.\n\n" f"Handle these trades manually on {self.exchange.name}, " f"or '/start' the bot again and use '/stopbuy' " @@ -634,7 +634,7 @@ class FreqtradeBot(LoggingMixin): """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -658,7 +658,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -678,7 +678,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy_fill(self, trade: Trade) -> None: msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'type': RPCMessageType.BUY_FILL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'open_rate': trade.open_rate, @@ -1239,8 +1239,8 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill - else RPCMessageType.SELL_NOTIFICATION), + 'type': (RPCMessageType.SELL_FILL if fill + else RPCMessageType.SELL), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1284,7 +1284,7 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index bf0b88f6c..e5c0dffba 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -31,15 +31,15 @@ logger = logging.getLogger(__name__) class RPCMessageType(Enum): - STATUS_NOTIFICATION = 'status' - WARNING_NOTIFICATION = 'warning' - STARTUP_NOTIFICATION = 'startup' - BUY_NOTIFICATION = 'buy' - BUY_FILL_NOTIFICATION = 'buy_fill' - BUY_CANCEL_NOTIFICATION = 'buy_cancel' - SELL_NOTIFICATION = 'sell' - SELL_FILL_NOTIFICATION = 'sell_fill' - SELL_CANCEL_NOTIFICATION = 'sell_cancel' + STATUS = 'status' + WARNING = 'warning' + STARTUP = 'startup' + BUY = 'buy' + BUY_FILL = 'buy_fill' + BUY_CANCEL = 'buy_cancel' + SELL = 'sell' + SELL_FILL = 'sell_fill' + SELL_CANCEL = 'sell_cancel' def __repr__(self): return self.value diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7977d68de..f819b55b4 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -67,7 +67,7 @@ class RPCManager: def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'Dry run is enabled. All trades are simulated.' }) stake_currency = config['stake_currency'] @@ -79,7 +79,7 @@ class RPCManager: exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'*Exchange:* `{exchange_name}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' @@ -88,13 +88,13 @@ class RPCManager: f'*Strategy:* `{strategy_name}`' }) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Searching for {stake_currency} pairs to buy and sell ' 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, + 'type': RPCMessageType.STARTUP, 'status': f'Using Protections: \n{prots}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4dceeb46c..778baea3c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -176,6 +176,53 @@ class Telegram(RPCHandler): """ self._updater.stop() + def _format_buy_msg(self, msg: Dict[str, Any]) -> str: + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") + + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + message += ")`" + return message + + def _format_sell_msg(self, msg: Dict[str, Any]) -> str: + msg['amount'] = round(msg['amount'], 8) + msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) + msg['duration'] = msg['close_date'].replace( + microsecond=0) - msg['open_date'].replace(microsecond=0) + msg['duration_min'] = msg['duration'].total_seconds() / 60 + + msg['emoji'] = self._get_sell_emoji(msg) + + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) + + # Check if all sell properties are available. + # This might not be the case if the message origin is triggered by /forcesell + if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) + and self._rpc._fiat_converter): + msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) + message += (' `({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) + return message + def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ @@ -186,75 +233,31 @@ class Telegram(RPCHandler): # Notification disabled return - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._rpc._fiat_converter: - msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) - else: - msg['stake_amount_fiat'] = 0 + if msg['type'] == RPCMessageType.BUY: + message = self._format_buy_msg(msg) - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" - f" (#{msg['trade_id']})\n" - f"*Amount:* `{msg['amount']:.8f}`\n" - f"*Open Rate:* `{msg['limit']:.8f}`\n" - f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") - - if msg.get('fiat_currency', None): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - message += ")`" - - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell' message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open buy Order for {pair} (#{trade_id}). " + "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' + message = ("\N{LARGE CIRCLE} *{exchange}:* " "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: - msg['amount'] = round(msg['amount'], 8) - msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) - msg['duration'] = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 + elif msg['type'] == RPCMessageType.SELL: + message = self._format_sell_msg(msg) - msg['emoji'] = self._get_sell_emoji(msg) - - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) - - # Check if all sell properties are available. - # This might not be the case if the message origin is triggered by /forcesell - if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._rpc._fiat_converter): - msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - message += (' `({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) - - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: - message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) - - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: - message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) - - elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + elif msg['type'] == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: + elif msg['type'] == RPCMessageType.WARNING: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: + elif msg['type'] == RPCMessageType.STARTUP: message = '{status}'.format(**msg) else: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index c7e012af5..24e1348f1 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -45,21 +45,21 @@ class Webhook(RPCHandler): """ Send a message to telegram channel """ try: - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + if msg['type'] == RPCMessageType.BUY: valuedict = self._config['webhook'].get('webhookbuy', None) - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_CANCEL: valuedict = self._config['webhook'].get('webhookbuycancel', None) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_FILL: valuedict = self._config['webhook'].get('webhookbuyfill', None) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL: valuedict = self._config['webhook'].get('webhooksell', None) - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_FILL: valuedict = self._config['webhook'].get('webhooksellfill', None) - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_CANCEL: valuedict = self._config['webhook'].get('webhooksellcancel', None) - elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION): + elif msg['type'] in (RPCMessageType.STATUS, + RPCMessageType.STARTUP, + RPCMessageType.WARNING): valuedict = self._config['webhook'].get('webhookstatus', None) else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 6996c932b..69a757fcf 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -71,7 +71,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -86,7 +86,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -124,7 +124,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] - rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, + rpc_manager.send_msg({'type': RPCMessageType.STARTUP, 'status': 'TestMessage'}) assert log_has( "Message type 'startup' not implemented by handler webhook.", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a3c823aac..accb94d34 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -686,7 +686,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -748,7 +748,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert msg_mock.call_count == 12 msg = msg_mock.call_args_list[2][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1198,7 +1198,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1243,7 +1243,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1261,7 +1261,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1291,7 +1291,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1328,7 +1328,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1340,7 +1340,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1357,7 +1357,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'running' }) assert msg_mock.call_args[0][0] == '*Status:* `running`' @@ -1366,7 +1366,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: def test_warning_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'message' }) assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' @@ -1375,7 +1375,7 @@ def test_warning_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': '*Custom:* `Hello World`' }) assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' @@ -1394,7 +1394,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1420,7 +1420,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 38d2fe539..0560f8d53 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -68,7 +68,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -89,7 +89,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -110,8 +110,8 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.BUY_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'open_rate': 0.005, 'stake_amount': 0.8, @@ -130,7 +130,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -155,7 +155,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell cancel msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -180,8 +180,8 @@ def test_send_msg_webhook(default_conf, mocker): # Test Sell fill msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.SELL_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'close_rate': 0.005, @@ -203,9 +203,9 @@ def test_send_msg_webhook(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) - for msgtype in [RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION]: + for msgtype in [RPCMessageType.STATUS, + RPCMessageType.WARNING, + RPCMessageType.STARTUP]: # Test notification msg = { 'type': msgtype, @@ -228,8 +228,8 @@ def test_exception_send_msg(default_conf, mocker, caplog): del default_conf["webhook"]["webhookbuy"] webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) - assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", + webhook.send_msg({'type': RPCMessageType.BUY}) + assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() @@ -238,7 +238,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 39c3f0561..25239d503 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2624,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'trade_id': 1, - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -2674,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2732,7 +2732,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2902,9 +2902,9 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 - assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION - assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION - assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2941,7 +2941,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', From d740aae8ca96a221a744b69fb16f708b8a632623 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:49:29 +0200 Subject: [PATCH 190/460] Default fill notifications to off --- freqtrade/constants.py | 14 ++++++++++++-- freqtrade/freqtradebot.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7b955c37d..aea6e1ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -246,14 +246,24 @@ CONF_SCHEMA = { 'balance_dust_level': {'type': 'number', 'minimum': 0.0}, 'notification_settings': { 'type': 'object', + 'default': {}, 'properties': { 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS} + 'buy_fill': {'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, + 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_fill': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, } } }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c48ea851e..ad55b38f8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,12 +1361,12 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - if not stoploss_order: + if not stoploss_order and not trade.open_order_id: self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() - elif trade.open_order_id is None: + elif not trade.open_order_id: # Buy fill self._notify_buy_fill(trade) From efbe0843be6a05d623a77c8e91adfce2158ca9f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 07:57:34 +0200 Subject: [PATCH 191/460] Add documentation for fill messages --- docs/telegram-usage.md | 9 ++++++++- tests/rpc/test_rpc_telegram.py | 8 ++++---- tests/test_freqtradebot.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 377977892..824cb17c7 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -82,12 +82,19 @@ Example configuration showing the different settings: "buy": "silent", "sell": "on", "buy_cancel": "silent", - "sell_cancel": "on" + "sell_cancel": "on", + "buy_fill": "off", + "sell_fill": "off" }, "balance_dust_level": 0.01 }, ``` +`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. +`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. +`*_fill` notifications are off by default and must be explicitly enabled. + + `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. ## Create a custom keyboard (command shortcut buttons) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index accb94d34..718c1d3a0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1335,8 +1335,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange.') msg_mock.reset_mock() telegram.send_msg({ @@ -1347,8 +1347,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: timeout.') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 25239d503..44791f928 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2263,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 From f821ef5aec8fb8321b75bfbe9ab33e0eed380ef5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 19:36:30 +0200 Subject: [PATCH 192/460] Final finetunings of rpc_fill messages --- freqtrade/rpc/telegram.py | 2 +- tests/conftest.py | 3 ++- tests/rpc/test_rpc_telegram.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 778baea3c..ffe7a7ceb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,7 +242,7 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " diff --git a/tests/conftest.py b/tests/conftest.py index cc4fe91f0..788586134 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -314,7 +314,8 @@ def get_default_conf(testdatadir): "telegram": { "enabled": True, "token": "token", - "chat_id": "0" + "chat_id": "0", + "notification_settings": {}, }, "datadir": str(testdatadir), "initial_state": "running", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 718c1d3a0..d72ba36ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1254,6 +1254,25 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'Reason: cancelled due to timeout.') +def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['buy_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.BUY_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'open_rate': 200, + 'stake_amount': 100, + 'amount': 0.5, + 'open_date': arrow.utcnow().datetime + }) + assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* ' + 'Buy order for ETH/USDT (#1) filled for 200.') + + def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From fd110c7d625fc8b5bc12445fe94998eb1f10eb31 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Tue, 20 Apr 2021 11:50:53 -0600 Subject: [PATCH 193/460] The error that it prints says the contrary to what was evaluated. ex. Trading stopped due to Max Drawdown 0.79 < 0.2 within 48 candles --- freqtrade/plugins/protections/max_drawdown_protection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d1c6b192d..67e204039 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -61,7 +61,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"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}" f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) From 5defd9a7f882d75a9bf2c457cdc3ddd94a8bb7e4 Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Tue, 20 Apr 2021 19:52:57 +0200 Subject: [PATCH 194/460] setup.sh: Install libpython3-dev on Debian/Ubuntu Python.h is required to build c modules for Python. --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index d0ca1f643..631c31df2 100755 --- a/setup.sh +++ b/setup.sh @@ -138,7 +138,7 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git + sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git libpython3-dev install_talib } From cfa9315e2a3dd1706bca0f09a4e1e48315912ec3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 20:29:53 +0200 Subject: [PATCH 195/460] Prevent out of candle ROI sells --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..a1d4a2578 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -239,7 +239,7 @@ class Backtesting: # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - return max(close_rate, sell_row[LOW_IDX]) + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... From bd92ce938c6b92c2092cd119b597fc29d26ec2ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Apr 2021 16:05:28 +0200 Subject: [PATCH 196/460] trade_history should paginate through results this avoids huge results --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 4 ++-- freqtrade/rpc/rpc.py | 10 ++++++---- scripts/rest_client.py | 14 ++++++++++---- tests/rpc/test_rpc_apiserver.py | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 12bee1cf2..e582f6aa8 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -199,6 +199,7 @@ class OpenTradeSchema(TradeSchema): class TradeResponse(BaseModel): trades: List[TradeSchema] trades_count: int + total_trades: int class ForceBuyResponse(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index ebfafc290..cb3a5a710 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -85,8 +85,8 @@ def status(rpc: RPC = Depends(get_rpc)): # Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) # on big databases. Correct response model: response_model=TradeResponse, @router.get('/trades', tags=['info', 'trading']) -def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_history(limit) +def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_history(min(limit, 500), offset=offset, order_by_id=True) @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e5c0dffba..a7a4dcf5c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -300,11 +300,12 @@ class RPC: 'data': data } - def _rpc_trade_history(self, limit: int) -> Dict: + def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ - if limit > 0: + order_by = Trade.id if order_by_id else Trade.close_date.desc() + if limit: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.close_date.desc()).limit(limit) + order_by).limit(limit).offset(offset) else: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( Trade.close_date.desc()).all() @@ -313,7 +314,8 @@ class RPC: return { "trades": output, - "trades_count": len(output) + "trades_count": len(output), + "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), } def _rpc_stats(self) -> Dict[str, Any]: diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 40b338ce8..900b784f2 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -200,13 +200,19 @@ class FtRestClient(): """ return self._get("logs", params={"limit": limit} if limit else 0) - def trades(self, limit=None): - """Return trades history. + def trades(self, limit=None, offset=None): + """Return trades history, sorted by id - :param limit: Limits trades to the X last trades. No limit to get all the trades. + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. :return: json object """ - return self._get("trades", params={"limit": limit} if limit else 0) + params = {} + if limit: + params['limit'] = limit + if offset: + params['offset'] = offset + return self._get("trades", params) def trade(self, trade_id): """Return specific trade diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6505629eb..d87d4e59d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -506,7 +506,7 @@ def test_api_trades(botclient, mocker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) - assert len(rc.json()) == 2 + assert len(rc.json()) == 3 assert rc.json()['trades_count'] == 0 create_mock_trades(fee) From 759bbd8e72fb72507669b1c67671eb3c67a95c64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:23:37 +0200 Subject: [PATCH 197/460] Update documentation about pagination --- docs/rest-api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 5c25e9eeb..514e0f719 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -124,7 +124,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stop` | Stops the trader. | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. -| `trades` | List last trades. +| `trades` | List last trades. Limited to 500 trades per call. | `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. @@ -280,9 +280,10 @@ trade :param trade_id: Specify which trade to get. trades - Return trades history. + Return trades history, sorted by id - :param limit: Limits trades to the X last trades. No limit to get all the trades. + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. version Return the version of the bot. @@ -290,6 +291,7 @@ version whitelist Show the current whitelist. + ``` ### OpenAPI interface From 05ce3acc466dda62f7f65bb80d172d972d5923a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:28:13 +0200 Subject: [PATCH 198/460] Improve tests for api_trades --- docs/rest-api.md | 2 -- tests/rpc/test_rpc_apiserver.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 514e0f719..a0029a44c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -290,8 +290,6 @@ version whitelist Show the current whitelist. - - ``` ### OpenAPI interface diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d87d4e59d..69d312e65 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -508,6 +508,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert_response(rc) assert len(rc.json()) == 3 assert rc.json()['trades_count'] == 0 + assert rc.json()['total_trades'] == 0 create_mock_trades(fee) Trade.query.session.flush() @@ -516,10 +517,12 @@ def test_api_trades(botclient, mocker, fee, markets): assert_response(rc) assert len(rc.json()['trades']) == 2 assert rc.json()['trades_count'] == 2 + assert rc.json()['total_trades'] == 2 rc = client_get(client, f"{BASE_URI}/trades?limit=1") assert_response(rc) assert len(rc.json()['trades']) == 1 assert rc.json()['trades_count'] == 1 + assert rc.json()['total_trades'] == 2 def test_api_trade_single(botclient, mocker, fee, ticker, markets): From 9f6f3e0862b27693706ecbb44be0856834d05717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:41:18 +0200 Subject: [PATCH 199/460] Address ZeroDivisionExceptiond closes #4764 closes #4617 --- freqtrade/persistence/models.py | 2 ++ tests/test_persistence.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49d3e2d62..e7fd488c7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -547,6 +547,8 @@ class LocalTrade(): rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) + if self.open_trade_value == 0.0: + return 0.0 profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3b90f368f..dad0e275e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -388,6 +388,9 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): # Test with a custom fee rate on the close trade assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + trade.open_trade_value = 0.0 + assert trade.calc_profit_ratio(fee=0.003) == 0.0 + @pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee): From 0233aa248e6fc9edfaa9567e7dd50941616d4d0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 17:22:16 +0200 Subject: [PATCH 200/460] Limit stake_amount to max available amount --- freqtrade/wallets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 889fe6fa8..4415e4d53 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -156,7 +156,9 @@ class Wallets: val_tied_up = Trade.total_open_trades_stakes() available_amount = self._get_available_stake_amount(val_tied_up) - return (available_amount + val_tied_up) / self._config['max_open_trades'] + # Theoretical amount can be above available amount - therefore limit to available amount! + return min((available_amount + val_tied_up) / self._config['max_open_trades'], + available_amount) def _check_available_stake_amount(self, stake_amount: float) -> float: """ From ba2d4d4656d2ecc929bd517786a2c0e325ea7c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 19:27:21 +0200 Subject: [PATCH 201/460] Reduce number of calls to `Trade.total_open_traes_stakes()` --- freqtrade/wallets.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4415e4d53..dba16cc35 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,7 +145,8 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. @@ -153,22 +154,17 @@ class Wallets: if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) - + possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] # Theoretical amount can be above available amount - therefore limit to available amount! - return min((available_amount + val_tied_up) / self._config['max_open_trades'], - available_amount) + return min(possible_stake, available_amount) - def _check_available_stake_amount(self, stake_amount: float) -> float: + def _check_available_stake_amount(self, stake_amount: float, available_amount: float) -> float: """ Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio @@ -195,17 +191,20 @@ class Wallets: stake_amount: float # Ensure wallets are uptodate. self.update() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if edge: stake_amount = edge.stake_amount( pair, self.get_free(self._config['stake_currency']), self.get_total(self._config['stake_currency']), - Trade.total_open_trades_stakes() + val_tied_up ) else: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: - stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) + stake_amount = self._calculate_unlimited_stake_amount( + free_open_trades, available_amount, val_tied_up) - return self._check_available_stake_amount(stake_amount) + return self._check_available_stake_amount(stake_amount, available_amount) From f7a4331c864bafc1d8454dfe7dbcb42393275768 Mon Sep 17 00:00:00 2001 From: onerobotband Date: Wed, 21 Apr 2021 18:38:57 +0100 Subject: [PATCH 202/460] Create config_ftx.json.example to stop the dl trades error from popping up all the time --- config_ftx.json.example | 99 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 config_ftx.json.example diff --git a/config_ftx.json.example b/config_ftx.json.example new file mode 100644 index 000000000..33e884976 --- /dev/null +++ b/config_ftx.json.example @@ -0,0 +1,99 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 1, + "use_sell_signal": true, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "exchange": { + "name": "ftx", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 50 + }, + "pair_whitelist": [ + "ALGO/BTC", + "ATOM/BTC", + "BAT/BTC", + "BCH/BTC", + "BRD/BTC", + "EOS/BTC", + "ETH/BTC", + "IOTA/BTC", + "LINK/BTC", + "LTC/BTC", + "NEO/BTC", + "NXS/BTC", + "XMR/BTC", + "XRP/BTC", + "XTZ/BTC" + ], + "pair_blacklist": [ + "BNB/BTC" + ] + }, + "pairlists": [ + {"method": "StaticPairList"} + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "jwt_secret_key": "somethingrandom", + "CORS_origins": [], + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} From d8c8a8d8c22923c301f8db7e1099a5422c5fe026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:01:10 +0200 Subject: [PATCH 203/460] Remvoe pointless arguments from get_trade_stake_amount --- freqtrade/freqtradebot.py | 3 +-- freqtrade/optimize/backtesting.py | 8 +++----- freqtrade/rpc/rpc.py | 3 +-- freqtrade/wallets.py | 8 ++++---- tests/optimize/test_backtesting.py | 10 +++++----- tests/test_freqtradebot.py | 16 ++++++---------- tests/test_integration.py | 6 ++---- tests/test_wallets.py | 12 ++++++------ 8 files changed, 28 insertions(+), 38 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..f370ff34f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -472,8 +472,7 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), - self.edge) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..71110b914 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,11 +273,9 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row: List, max_open_trades: int, - open_trade_count: int) -> Optional[LocalTrade]: + def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: - stake_amount = self.wallets.get_trade_stake_amount( - pair, max_open_trades - open_trade_count, None) + stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) @@ -388,7 +386,7 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): - trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) + trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..eedb1c510 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -603,8 +603,7 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.wallets.get_trade_stake_amount( - pair, self._freqtrade.get_free_open_trades()) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index dba16cc35..bbbe5ba5e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,13 +145,13 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + def _calculate_unlimited_stake_amount(self, available_amount: float, val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades or self._config['max_open_trades'] == 0: + if self._config['max_open_trades'] == 0: return 0 possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] @@ -182,7 +182,7 @@ class Wallets: return stake_amount - def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: + def get_trade_stake_amount(self, pair: str, edge=None) -> float: """ Calculate stake amount for the trade :return: float: Stake amount @@ -205,6 +205,6 @@ class Wallets: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: stake_amount = self._calculate_unlimited_stake_amount( - free_open_trades, available_amount, val_tied_up) + available_amount, val_tied_up) return self._check_available_stake_amount(stake_amount, available_amount) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4bbfe8a78..41d4207c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -457,7 +457,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) -def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: +def test_backtest__enter_trade(default_conf, fee, mocker) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) @@ -474,24 +474,24 @@ def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: 0.00099, # Low 0.0012, # High ] - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + trade = backtesting._enter_trade(pair, row=row) assert trade is None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..b0fa2d6c2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -160,8 +160,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.wallets.get_trade_stake_amount( - 'ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert result == default_conf['stake_amount'] @@ -197,14 +196,12 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b if expected[i] is not None: limit_buy_order_open['id'] = str(i) - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') def test_edge_called_in_process(mocker, edge_conf) -> None: @@ -230,9 +227,9 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: freqtrade = FreqtradeBot(edge_conf) assert freqtrade.wallets.get_trade_stake_amount( - 'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 + 'NEO/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 assert freqtrade.wallets.get_trade_stake_amount( - 'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 + 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -448,8 +445,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), - freqtrade.edge) == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, diff --git a/tests/test_integration.py b/tests/test_integration.py index 1c60faa7b..be0dd1137 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -177,8 +177,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 rpc._rpc_forcebuy('TKN/BTC', None) @@ -199,8 +198,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc # One trade sold assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 86f49698b..ff303e2ec 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -118,7 +118,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') @pytest.mark.parametrize("balance_ratio,result1,result2", [ @@ -145,28 +145,28 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT') assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') assert result == 0 freqtrade.config['max_open_trades'] = 3 freqtrade.config['dry_run_wallet'] = 200 freqtrade.wallets.start_cap = 200 - result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT') assert round(result, 4) == round(result2, 4) # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT') assert result == 0 From 92a2e254af2c0182840830f3c0f2b9ba10d566cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:17:30 +0200 Subject: [PATCH 204/460] Fix backtesting test --- tests/optimize/test_backtesting.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 41d4207c3..00114be5b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -463,6 +463,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) default_conf['stake_amount'] = 'unlimited' + default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' row = [ @@ -478,8 +479,14 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 + # Fake 2 trades, so there's not enough amount for the next trade left. + LocalTrade.trades_open.append(trade) + LocalTrade.trades_open.append(trade) trade = backtesting._enter_trade(pair, row=row) assert trade is None + LocalTrade.trades_open.pop() + trade = backtesting._enter_trade(pair, row=row) + assert trade is not None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) @@ -487,7 +494,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None - # Stake-amount too high! + # Stake-amount throwing error mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) From 896ec58cadd3d94eb8d61077e3ec07be1c895b48 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Wed, 21 Apr 2021 15:02:33 -0600 Subject: [PATCH 205/460] Add the reason why there is a global pairlock when lock is available --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ad55b38f8..a64d3f4cc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -378,7 +378,7 @@ class FreqtradeBot(LoggingMixin): 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) + f"Not creating new trades, reason: {lock.reason}.", logger.info) else: self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created From 515c73f3990ee976baea1c8dd959a090d64a2c86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 06:51:26 +0200 Subject: [PATCH 206/460] Don't hard-limit trades endpoint for now --- freqtrade/rpc/api_server/api_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index cb3a5a710..e907b92f0 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -86,7 +86,7 @@ def status(rpc: RPC = Depends(get_rpc)): # on big databases. Correct response model: response_model=TradeResponse, @router.get('/trades', tags=['info', 'trading']) def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_history(min(limit, 500), offset=offset, order_by_id=True) + return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True) @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) From 09efa7b06b7cd39726d07c9f1f231249651271c8 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 10:07:13 +0300 Subject: [PATCH 207/460] Add --new-pairs-days parameter for download-data command. This parameter allows us to customize a number of days we would like to download for new pairs only. This allows us to achieve efficient data update, downloading all data for new pairs and only missing data for existing pairs. To do that use `freqtrade download-data --new-pairs-days=3650` (not specifying `--days` or `--timerange` causes freqtrade to download only missing data for existing pairs). --- docs/data-download.md | 6 ++++-- freqtrade/commands/arguments.py | 5 +++-- freqtrade/commands/cli_options.py | 7 +++++++ freqtrade/commands/data_commands.py | 9 +++++---- freqtrade/configuration/configuration.py | 7 +++++++ freqtrade/data/history/history_utils.py | 15 ++++++++++----- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7a78334d5..7b09cf49c 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -11,8 +11,9 @@ 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. !!! Tip "Tip: Updating existing data" - If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. - Be careful though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. + If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data. + If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only. + If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data. ### Usage @@ -34,6 +35,7 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. + --new-pairs-days INT Download data of new pairs for given number of days. Default: `30`. --timerange TIMERANGE Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9cf9992ce..ffd317799 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -60,8 +60,9 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] -ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "timerange", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange", + "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", + "dataformat_trades"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index e49895de4..80c56ecfa 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -345,6 +345,13 @@ AVAILABLE_CLI_OPTIONS = { type=check_int_positive, metavar='INT', ), + "new_pairs_days": Arg( + '--new-pairs-days', + help='Download data of new pairs for given number of days. Default: `%(default)s`.', + type=check_int_positive, + metavar='INT', + default=30, + ), "download_trades": Arg( '--dl-trades', help='Download trades instead of OHLCV data. The bot will resample trades to the ' diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 1ce02eee5..58191ddb4 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -62,8 +62,8 @@ def start_download_data(args: Dict[str, Any]) -> None: if config.get('download_trades'): pairs_not_available = refresh_backtest_trades_data( exchange, pairs=expanded_pairs, datadir=config['datadir'], - timerange=timerange, erase=bool(config.get('erase')), - data_format=config['dataformat_trades']) + timerange=timerange, new_pairs_days=config['new_pairs_days'], + erase=bool(config.get('erase')), data_format=config['dataformat_trades']) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( @@ -75,8 +75,9 @@ def start_download_data(args: Dict[str, Any]) -> None: else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=expanded_pairs, timeframes=config['timeframes'], - datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), - data_format=config['dataformat_ohlcv']) + datadir=config['datadir'], timerange=timerange, + new_pairs_days=config['new_pairs_days'], + erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv']) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cc11f97c2..86f337c1b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -108,6 +108,8 @@ class Configuration: self._process_plot_options(config) + self._process_data_options(config) + # Check if the exchange set by the user is supported check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) @@ -399,6 +401,11 @@ class Configuration: self._args_to_config(config, argname='dataformat_trades', logstring='Using "{}" to store trades data.') + def _process_data_options(self, config: Dict[str, Any]) -> None: + + self._args_to_config(config, argname='new_pairs_days', + logstring='Detected --new-pairs-days: {}') + def _process_runmode(self, config: Dict[str, Any]) -> None: self._args_to_config(config, argname='dry_run', diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 3b8b5a2f0..58965abe0 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -155,6 +155,7 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona def _download_pair_history(datadir: Path, exchange: Exchange, pair: str, *, + new_pairs_days: int = 30, timeframe: str = '5m', timerange: Optional[TimeRange] = None, data_handler: IDataHandler = None) -> bool: @@ -193,7 +194,7 @@ def _download_pair_history(datadir: Path, timeframe=timeframe, since_ms=since_ms if since_ms else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000 + days=-new_pairs_days).float_timestamp) * 1000 ) # TODO: Maybe move parsing to exchange class (?) new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, @@ -223,7 +224,8 @@ def _download_pair_history(datadir: Path, def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], datadir: Path, timerange: Optional[TimeRange] = None, - erase: bool = False, data_format: str = None) -> List[str]: + new_pairs_days: int = 30, erase: bool = False, + data_format: str = None) -> List[str]: """ Refresh stored ohlcv data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -246,12 +248,14 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes logger.info(f'Downloading pair {pair}, interval {timeframe}.') _download_pair_history(datadir=datadir, exchange=exchange, pair=pair, timeframe=str(timeframe), + new_pairs_days=new_pairs_days, timerange=timerange, data_handler=data_handler) return pairs_not_available def _download_trades_history(exchange: Exchange, pair: str, *, + new_pairs_days: int = 30, timerange: Optional[TimeRange] = None, data_handler: IDataHandler ) -> bool: @@ -263,7 +267,7 @@ def _download_trades_history(exchange: Exchange, since = timerange.startts * 1000 if \ (timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000 + days=-new_pairs_days).float_timestamp) * 1000 trades = data_handler.trades_load(pair) @@ -311,8 +315,8 @@ def _download_trades_history(exchange: Exchange, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, - timerange: TimeRange, erase: bool = False, - data_format: str = 'jsongz') -> List[str]: + timerange: TimeRange, new_pairs_days: int = 30, + erase: bool = False, data_format: str = 'jsongz') -> List[str]: """ Refresh stored trades data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -333,6 +337,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: logger.info(f'Downloading trades for pair {pair}.') _download_trades_history(exchange=exchange, pair=pair, + new_pairs_days=new_pairs_days, timerange=timerange, data_handler=data_handler) return pairs_not_available From f744df2374b8a7edb450277f640f815a0eca647f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 10:01:41 +0200 Subject: [PATCH 208/460] Fix bad fill message --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ffe7a7ceb..cb3dbe6c8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -246,7 +246,8 @@ class Telegram(RPCHandler): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + "{message_side} order for {pair} (#{trade_id}) filled " + "for {open_rate}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL: message = self._format_sell_msg(msg) From 3144185409ddeb36529b693a7bcb19df400aac20 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 11:18:28 +0300 Subject: [PATCH 209/460] Allow specifying "new_pairs_days" in config. --- freqtrade/commands/cli_options.py | 1 - freqtrade/constants.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 80c56ecfa..b583b47ba 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -350,7 +350,6 @@ AVAILABLE_CLI_OPTIONS = { help='Download data of new pairs for given number of days. Default: `%(default)s`.', type=check_int_positive, metavar='INT', - default=30, ), "download_trades": Arg( '--dl-trades', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index aea6e1ff2..77bd2a029 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +NEW_PAIRS_DAYS = 30 DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -96,6 +97,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, + 'new_pairs_days': {'type': 'integer', 'default': NEW_PAIRS_DAYS}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { From 7e2e196643cfa5bf6cf5aa76c55fab33139f65f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 17:13:22 +0200 Subject: [PATCH 210/460] improve sell_message by using sell rate --- freqtrade/rpc/telegram.py | 11 ++++++----- tests/rpc/test_rpc_telegram.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cb3dbe6c8..3eeedcd12 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,13 +242,14 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): - msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' - + elif msg['type'] == RPCMessageType.BUY_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " - "{message_side} order for {pair} (#{trade_id}) filled " + "Buy order for {pair} (#{trade_id}) filled " "for {open_rate}.".format(**msg)) - + elif msg['type'] == RPCMessageType.SELL_FILL: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled " + "for {close_rate}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL: message = self._format_sell_msg(msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d72ba36ad..6a36c12a7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1372,6 +1372,35 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = old_convamount +def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['sell_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.SELL_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'gain': 'loss', + 'limit': 3.201e-05, + 'amount': 0.1, + 'order_type': 'market', + 'open_rate': 500, + 'close_rate': 550, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD', + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(hours=-1), + 'close_date': arrow.utcnow(), + }) + assert msg_mock.call_args[0][0] \ + == ('\N{LARGE CIRCLE} *Binance:* Sell order for ETH/USDT (#1) filled for 550.') + + def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From 0d2457cd478978dfbad3d2de526be03170176c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 19:28:39 +0200 Subject: [PATCH 211/460] Add lock_reason to per-pair lock --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a64d3f4cc..c3a4bc0e0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -456,7 +456,8 @@ class FreqtradeBot(LoggingMixin): 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)}.", + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " + f"due to {lock.reason}.", logger.info) else: self.log_once(f"Pair {pair} is still locked.", logger.info) From ccaf5764da5f9d63c77bb901a0b89541cb3ea661 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 19:41:01 +0200 Subject: [PATCH 212/460] Small adjustments --- docs/data-download.md | 8 +++++--- freqtrade/constants.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7b09cf49c..01561c89b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -21,8 +21,9 @@ You can use a relative timerange (`--days 20`) or an absolute starting point (`- usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] - [--days INT] [--timerange TIMERANGE] - [--dl-trades] [--exchange EXCHANGE] + [--days INT] [--new-pairs-days INT] + [--timerange TIMERANGE] [--dl-trades] + [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] @@ -35,7 +36,8 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. - --new-pairs-days INT Download data of new pairs for given number of days. Default: `30`. + --new-pairs-days INT Download data of new pairs for given number of days. + Default: `None`. --timerange TIMERANGE Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 77bd2a029..bfd1e72f1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -NEW_PAIRS_DAYS = 30 DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -97,7 +96,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, - 'new_pairs_days': {'type': 'integer', 'default': NEW_PAIRS_DAYS}, + 'new_pairs_days': {'type': 'integer', 'default': 30}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { From 406c1267a2cc2f396656be2ea0d96293c8335d88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 20:01:08 +0200 Subject: [PATCH 213/460] Remove superfluss space --- freqtrade/data/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index c9d4ef19f..af6c6a2ef 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 if len_before != len_after: message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" - f" - {round(pct_missing * 100, 2)} %") + f" - {round(pct_missing * 100, 2)}%") if pct_missing > 0.01: logger.info(message) else: From 4005708f85b8b1885143168e5f9cf0323d34ef09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 06:50:39 +0200 Subject: [PATCH 214/460] Handle edge with volumepairlist and empty pair_whitelist closes #4779 --- freqtrade/edge/edge_positioning.py | 9 +++++++-- tests/edge/test_edge.py | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index d1f76c21f..334aabfab 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -81,10 +81,15 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee(symbol=expand_pairlist( - self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) + try: + self.fee = self.exchange.get_fee(symbol=expand_pairlist( + self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) + except IndexError: + self.fee = None def calculate(self, pairs: List[str]) -> bool: + if self.fee is None and pairs: + self.fee = self.exchange.get_fee(pairs[0]) heartbeat = self.edge_config.get('process_throttle_secs') diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 5142dd985..25e0da5e2 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -330,11 +330,11 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) - mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data', ) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty - mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[]) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) @@ -342,6 +342,23 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): assert log_has("No trades found.", caplog) +def test_edge_process_no_pairs(mocker, edge_conf, caplog): + edge_conf['exchange']['pair_whitelist'] = [] + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data') + mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) + # Return empty + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[]) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + assert fee_mock.call_count == 0 + assert edge.fee is None + + assert not edge.calculate(['XRP/USDT']) + assert fee_mock.call_count == 1 + assert edge.fee == 0.001 + + def test_edge_init_error(mocker, edge_conf,): edge_conf['stake_amount'] = 0.5 mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) From b69a9134f59f3eed093dc0fad8c5b7191cdcb888 Mon Sep 17 00:00:00 2001 From: saeedrss Date: Fri, 23 Apr 2021 21:27:13 +0430 Subject: [PATCH 215/460] fixing support for HitBTC #4778 hitbtc by default send candle from beginning (not most recently) this change fixed --- freqtrade/exchange/exchange.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed7918b36..809cdb4e1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -874,8 +874,15 @@ class Exchange: "Fetching pair %s, interval %s, since %s %s...", pair, timeframe, since_ms, s ) - - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, + #fixing support for HitBTC #4778 + if self.name== 'HitBTC': + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, + since=since_ms, + limit=self.ohlcv_candle_limit(timeframe), + params={"sort": "DESC"} + ) + else: + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, limit=self.ohlcv_candle_limit(timeframe)) From df16fbd742bd6b0f11a9391c57f8a828f9bc5d68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 19:22:41 +0200 Subject: [PATCH 216/460] Add "dataload complete" message to backtest + hyperopt --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a1d4a2578..4731e6a38 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -478,6 +478,7 @@ class Backtesting: data: Dict[str, Any] = {} data, timerange = self.load_bt_data() + logger.info("Dataload complete. Calculating indicators") for strat in self.strategylist: min_date, max_date = self.backtest_one_strategy(strat, data, timerange) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d6003cf86..d1dabff36 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,7 @@ class Hyperopt: logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 data, timerange = self.backtesting.load_bt_data() - + logger.info("Dataload complete. Calculating indicators") preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe From 191a31db30403fcef3dad510167c45c41599c833 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 19:36:26 +0200 Subject: [PATCH 217/460] NameErrors should not stop loading a different strategy --- freqtrade/resolvers/iresolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 37cfd70e6..b51795e9e 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -61,7 +61,7 @@ class IResolver: module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError) as err: + except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") if enum_failed: From 9dc7f776d98a0e1db3412b1baa76ed8c93055d5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 20:35:30 +0200 Subject: [PATCH 218/460] Improve log output when loading parameters --- freqtrade/strategy/hyper.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 16b576a73..988eae531 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -260,12 +260,15 @@ class HyperStrategyMixin(object): :param params: Dictionary with new parameter values. """ if not params: - return + logger.info(f"No params for {space} found, using default values.") + for attr_name, attr in self.enumerate_parameters(): - if attr_name in params: + if params and attr_name in params: if attr.load: attr.value = params[attr_name] logger.info(f'Strategy Parameter: {attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') + else: + logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') From 90476c4287aebacc7dc148950f420cffe8e8f038 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 07:00:33 +0200 Subject: [PATCH 219/460] Add "range" property to IntParameter --- freqtrade/strategy/hyper.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 988eae531..32486136d 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -5,7 +5,7 @@ This module defines a base class for auto-hyperoptable strategies. import logging from abc import ABC, abstractmethod from contextlib import suppress -from typing import Any, Iterator, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): @@ -13,6 +13,7 @@ with suppress(ImportError): from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any + hyperopt: bool = False def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -121,6 +123,20 @@ class IntParameter(NumericParameter): """ return Integer(low=self.low, high=self.high, name=name, **self._space_params) + @property + def range(self): + """ + Get each value in this space as list. + Returns a List from low to high (inclusive) in Hyperopt mode. + Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid + calculating 100ds of indicators. + """ + if self.hyperopt: + # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive + return range(self.low, self.high + 1) + else: + return range(self.value, self.value + 1) + class RealParameter(NumericParameter): default: float @@ -227,12 +243,11 @@ class HyperStrategyMixin(object): strategy logic. """ - def __init__(self, *args, **kwargs): + def __init__(self, config: Dict[str, Any], *args, **kwargs): """ Initialize hyperoptable strategy mixin. """ - self._load_params(getattr(self, 'buy_params', None)) - self._load_params(getattr(self, 'sell_params', None)) + self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ @@ -254,7 +269,14 @@ class HyperStrategyMixin(object): (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr - def _load_params(self, params: dict) -> None: + def _load_hyper_params(self, hyperopt: bool = False) -> None: + """ + Load Hyperoptable parameters + """ + self._load_params(getattr(self, 'buy_params', None), 'buy', hyperopt) + self._load_params(getattr(self, 'sell_params', None), 'sell', hyperopt) + + def _load_params(self, params: dict, space: str, hyperopt: bool = False) -> None: """ Set optimizeable parameter values. :param params: Dictionary with new parameter values. @@ -263,6 +285,7 @@ class HyperStrategyMixin(object): logger.info(f"No params for {space} found, using default values.") for attr_name, attr in self.enumerate_parameters(): + attr.hyperopt = hyperopt if params and attr_name in params: if attr.load: attr.value = params[attr_name] From 5c7f278c8a21a7744978acd2ec4f8c93abffe1f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 07:18:35 +0200 Subject: [PATCH 220/460] add tests for IntParameter.range --- tests/optimize/conftest.py | 2 ++ tests/optimize/test_hyperopt.py | 9 +++++++++ tests/strategy/test_interface.py | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 5c789ec1e..11b4674f3 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -6,6 +6,7 @@ import pandas as pd import pytest from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -15,6 +16,7 @@ def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ 'datadir': Path(default_conf['datadir']), + 'runmode': RunMode.HYPEROPT, 'hyperopt': 'DefaultHyperOpt', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 59bc4aefb..f725a5581 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -21,6 +21,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode +from freqtrade.strategy.hyper import IntParameter from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -1103,6 +1104,14 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: }) hyperopt = Hyperopt(hyperopt_conf) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) + + assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True + assert hyperopt.backtesting.strategy.buy_rsi.value == 35 + buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert isinstance(buy_rsi_range, range) + # Range from 0 - 50 (inclusive) + assert len(list(buy_rsi_range)) == 51 hyperopt.start() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 78fa368e4..347d35b19 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -588,6 +588,14 @@ def test_hyperopt_parameters(): intpar = IntParameter(low=0, high=5, default=1, space='buy') assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) + assert isinstance(intpar.range, range) + assert len(list(intpar.range)) == 1 + # Range contains ONLY the default / value. + assert list(intpar.range) == [intpar.value] + intpar.hyperopt = True + + assert len(list(intpar.range)) == 6 + assert list(intpar.range) == [0, 1, 2, 3, 4, 5] fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) From d647b841f0191b4a3c2a031452b96ebe1306403d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 09:03:59 +0200 Subject: [PATCH 221/460] Add docs how to optimize indicator parameters --- docs/hyperopt.md | 179 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 40 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 51905e616..bea8dc256 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -165,11 +165,22 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. - ```python + ``` bash # Have a working strategy at hand. freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` +### Hyperopt execution logic + +Hyperopt will first load your data into memory and will then run `populate_indicators()` once per Pair to generate all indicators. + +Hyperopt will then spawn into different processes (number of processors, or `-j `), and run backtesting over and over again, changing the parameters that are part of the `--spaces` defined. + +For every new set of parameters, freqtrade will run first `populate_buy_trend()` followed by `populate_sell_trend()`, and then run the regular backtesting process to simulate trades. + +After backtesting, the results are passed into the [loss function](#loss-functions), which will evaluate if this result was better or worse than previous results. +Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting. + ### Configure your Guards and Triggers There are two places you need to change in your strategy file to add a new buy hyperopt for testing: @@ -188,59 +199,54 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if -ADX > 10*". - -```python -from freqtrade.strategy import IntParameter, IStrategy - -class MyAwesomeStrategy(IStrategy): - # If parameter is prefixed with `buy_` or `sell_` then specifying `space` parameter is optional - # and space is inferred from parameter name. - buy_adx_min = IntParameter(0, 100, default=10) - - def populate_buy_trend(self, dataframe: 'DataFrame', metadata: dict) -> 'DataFrame': - dataframe.loc[ - ( - (dataframe['adx'] > self.buy_adx_min.value) - ), 'buy'] = 1 - return dataframe -``` +multiple guards. #### Sell optimization Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods -* Define the parameters at the class level hyperopt shall be optimizing. +* Define the parameters at the class level hyperopt shall be optimizing, either naming them `sell_*`, or by explicitly defining `space='sell'`. * Within `populate_sell_trend()` - use defined parameter values instead of raw constants. The configuration and rules are the same than for buy signals. -```python -class MyAwesomeStrategy(IStrategy): - # There is no strict parameter naming scheme. If you do not use `buy_` or `sell_` prefixes - - # please specify to which space parameter belongs using `space` parameter. Possible values: - # 'buy' or 'sell'. - adx_max = IntParameter(0, 100, default=50, space='sell') +## Solving a Mystery - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - (dataframe['adx'] < self.adx_max.value) - ), 'buy'] = 1 +Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys. +And you also wonder should you use RSI or ADX to help with those buy decisions. +If you decide to use RSI or ADX, which values should I use for them? + +So let's use hyperparameter optimization to solve this mystery. + +### Defining indicators to be used + +We start by calculating the indicators our strategy is going to use. + +``` python +class MyAwesomeStrategy(IStrategy): + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Generate all indicators used by the strategy + """ + dataframe['adx'] = ta.ADX(dataframe) + dataframe['rsi'] = ta.RSI(dataframe) + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) + dataframe['bb_lowerband'] = boll['lowerband'] + dataframe['bb_middleband'] = boll['middleband'] + dataframe['bb_upperband'] = boll['upperband'] return dataframe ``` -## Solving a Mystery +### Hyperoptable parameters -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining hyperoptable parameters: +We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): @@ -260,6 +266,8 @@ The last one we call `trigger` and use it to decide which buy trigger we want to So let's write the buy strategy using these values: ```python + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -288,7 +296,7 @@ So let's write the buy strategy using these values: ``` Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. +It will use the given historical data and simulate buys based on the buy signals generated with the above function. Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). !!! Note @@ -314,6 +322,87 @@ There are four parameter types each suited for different purposes. !!! Warning Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. +### Optimizing an indicator parameter + +Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. + +``` python +import talib.abstract as ta + +from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + stoploss = 0.5 + timeframe = '15m' + # Define the parameter spaces + buy_ema_short = IntParameter(3, 50, default=5) + buy_ema_long = IntParameter(15, 200, default=50) + + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """Generate all indicators used by the strategy""" + + # Calculate all ema_short values + for val in self.buy_ema_short.range: + dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val) + + # Calculate all ema_long values + for val in self.buy_ema_long.range: + dataframe[f'ema_long_{val}'] = ta.EMA(dataframe, timeperiod=val) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + conditions.append(qtpylib.crossed_above( + dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + conditions.append(qtpylib.crossed_above( + dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'sell'] = 1 + return dataframe +``` + +Breaking it down: + +Using `self.buy_ema_short.range` will return a range object containing all entries between the Parameters low and high value. +In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`). +By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`). + +Hyperopt itself will then use the selected value to create the buy and sell signals + +While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters. + +!!! Note + `self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`). + +??? Hint "Performance tip" + By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter. + While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values). + You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. @@ -606,6 +695,16 @@ number). You can also enable position stacking in the configuration file by explicitly setting `"position_stacking"=true`. +## Out of Memory errors + +As hyperopt consumes a lot of memory (the complete data needs to be in memory once per parallel backtesting process), it's likely that you run into "out of memory" errors. +To combat these, you have multiple options: + +* reduce the amount of pairs +* reduce the timerange used (`--timerange `) +* reduce the number of parallel processes (`-j `) +* Increase the memory of your machine + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. From 7453dac668274f02dbff38a0d6dea69197b3b9f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 13:13:41 +0200 Subject: [PATCH 222/460] Improve doc wording --- docs/hyperopt.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index bea8dc256..b3fdc699b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -198,8 +198,7 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. However, this guide will make this distinction to make it clear that signals should not be "sticking". Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). -Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. +Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. #### Sell optimization @@ -266,8 +265,6 @@ The last one we call `trigger` and use it to decide which buy trigger we want to So let's write the buy strategy using these values: ```python - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -327,6 +324,9 @@ There are four parameter types each suited for different purposes. Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. ``` python +from pandas import DataFrame +from functools import reduce + import talib.abstract as ta from freqtrade.strategy import IStrategy @@ -334,7 +334,7 @@ from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParame import freqtrade.vendor.qtpylib.indicators as qtpylib class MyAwesomeStrategy(IStrategy): - stoploss = 0.5 + stoploss = -0.05 timeframe = '15m' # Define the parameter spaces buy_ema_short = IntParameter(3, 50, default=5) From 31b0e3b5e8cc672c7a94a0e708b91a96e870dc9e Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Sat, 24 Apr 2021 13:25:28 +0200 Subject: [PATCH 223/460] add distribution graph to example notebook --- .../templates/strategy_analysis_example.ipynb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 491afbdd7..0bc593e2d 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -282,6 +282,28 @@ "graph.show(renderer=\"browser\")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot average profit per trade as distribution graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.figure_factory as ff\n", + "\n", + "hist_data = [trades.profit_ratio]\n", + "group_labels = ['profit_ratio'] # name of the dataset\n", + "\n", + "fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01)\n", + "fig.show()\n" + ] + }, { "cell_type": "markdown", "metadata": {}, From 185d754b8bd827d59e4eafae3edc4f6fe85077f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 13:39:20 +0200 Subject: [PATCH 224/460] Improve documentation to suggest config-private.json --- docs/configuration.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0ade558f1..37395c5ee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,16 @@ Per default, the bot loads the configuration from the `config.json` file, locate You can specify a different configuration file used by the bot with the `-c/--config` command line option. -In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. +Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. + +!!! Tip "Use multiple configuration files to keep secrets secret" + You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself. + + ``` bash + freqtrade trade --config user_data/config.json --config user_data/config-private.json <...> + ``` + The 2nd file should only specify what you intend to override. + If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`). If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. @@ -518,16 +527,27 @@ API Keys are usually only required for live trading (trading for real money, bot **Insert your Exchange API key (change them by fake api keys):** ```json -"exchange": { +{ + "exchange": { "name": "bittrex", "key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", - ... + //"password": "", // Optional, not needed by all exchanges) + // ... + } + //... } ``` You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. +!!! Hint "Keep your secrets secret" + To keep your secrets secret, we recommend to use a 2nd configuration for your API keys. + Simply use the above snippet in a new configuration file (e.g. `config-private.json`) and keep your settings in this file. + You can then start the bot with `freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>` to have your keys loaded. + + **NEVER** share your private configuration file or your exchange keys with anyone! + ### Using proxy with Freqtrade To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. From b223775385684c0698a143538ad8acb9086730ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 15:56:53 +0200 Subject: [PATCH 225/460] Update "output" of jupyter notebook as well --- docs/strategy_analysis_example.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 5c479aa0b..4c938500c 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -195,4 +195,18 @@ graph.show(renderer="browser") ``` +## Plot average profit per trade as distribution graph + + +```python +import plotly.figure_factory as ff + +hist_data = [trades.profit_ratio] +group_labels = ['profit_ratio'] # name of the dataset + +fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01) +fig.show() + +``` + Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. From 88f26971fa6fdfc7ac05868b509d4d301fb4be8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 19:15:09 +0200 Subject: [PATCH 226/460] Use defaultdict for backtesting --- freqtrade/optimize/backtesting.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fc5c0fdd7..fb9826a23 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -352,7 +352,7 @@ class Backtesting: data: Dict = self._get_ohlcv_as_lists(processed) # Indexes per pair, so some pairs are allowed to have a missing start. - indexes: Dict = {} + indexes: Dict = defaultdict(int) tmp = start_date + timedelta(minutes=self.timeframe_min) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) @@ -363,9 +363,6 @@ class Backtesting: open_trade_count_start = open_trade_count for i, pair in enumerate(data): - if pair not in indexes: - indexes[pair] = 0 - try: row = data[pair][indexes[pair]] except IndexError: From cb86c90d3ef4047c50f0fdb6392f03cc94cd86ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 19:16:54 +0200 Subject: [PATCH 227/460] Remove obsolete TODO's --- freqtrade/configuration/configuration.py | 2 -- freqtrade/exchange/exchange.py | 1 - 2 files changed, 3 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 86f337c1b..f6d0520c5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -75,8 +75,6 @@ class Configuration: # Normalize config if 'internals' not in config: config['internals'] = {} - # TODO: This can be deleted along with removal of deprecated - # experimental settings if 'ask_strategy' not in config: config['ask_strategy'] = {} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed7918b36..80b392d73 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -363,7 +363,6 @@ class Exchange: invalid_pairs = [] for pair in extended_pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs - # TODO: add a support for having coins in BTC/USDT format if self.markets and pair not in self.markets: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' From e855530483d7c2ff6cbb8c813ed050565833cbbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Apr 2021 20:26:37 +0200 Subject: [PATCH 228/460] hdf5 handler should include the end-date --- freqtrade/data/history/hdf5datahandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index d116637e7..e80cfeba2 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -89,7 +89,7 @@ class HDF5DataHandler(IDataHandler): if timerange.starttype == 'date': where.append(f"date >= Timestamp({timerange.startts * 1e9})") if timerange.stoptype == 'date': - where.append(f"date < Timestamp({timerange.stopts * 1e9})") + where.append(f"date <= Timestamp({timerange.stopts * 1e9})") pairdata = pd.read_hdf(filename, key=key, mode="r", where=where) From 2eda25426f8b3bdfc6031a437456079e5ed8f8b2 Mon Sep 17 00:00:00 2001 From: wr0ngc0degen Date: Sun, 25 Apr 2021 05:47:59 +0200 Subject: [PATCH 229/460] fix typo in sample_strategy.py fix copy-paste issue in populate_sell_trend docstring --- freqtrade/templates/sample_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index a51b30f3f..e51feff1e 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -361,7 +361,7 @@ class SampleStrategy(IStrategy): Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame populated with indicators :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column + :return: DataFrame with sell column """ dataframe.loc[ ( From 4636b3970b29a1a82c1a1fc56f46453b7ee1f019 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 08:25:12 +0200 Subject: [PATCH 230/460] Fix failed test due to exchange downtime --- tests/optimize/test_hyperopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f725a5581..90ff8a1d0 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1094,7 +1094,9 @@ def test_print_epoch_details(capsys): assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) -def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: +def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed del hyperopt_conf['hyperopt'] From 0fd68aee51ad010a7155aba0ff0caee8f62d57ef Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Apr 2021 10:49:09 +0300 Subject: [PATCH 231/460] Add IStrategy.custom_sell method which allows per-trade sell signal evaluation. --- freqtrade/strategy/interface.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 54c7f2353..b0710e833 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -285,6 +285,27 @@ class IStrategy(ABC, HyperStrategyMixin): """ return self.stoploss + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> bool: + """ + Custom sell signal logic indicating that specified position should be sold. Returning True + from this method is equal to setting sell signal on a candle at specified time. + This method is not called when sell signal is set. + + This method should be overridden to create sell signals that depend on trade parameters. For + example you could implement a stoploss relative to candle when trade was opened, or a custom + 1:2 risk-reward ROI. + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: Whether trade should exit now. + """ + return False + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -535,9 +556,12 @@ class IStrategy(ABC, HyperStrategyMixin): and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False - else: - sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True) + elif ask_strategy.get('use_sell_signal', True): + sell = sell or self.custom_sell(trade.pair, trade, date, current_rate, current_profit) + sell_signal = sell and not buy # TODO: return here if sell-signal should be favored over ROI + else: + sell_signal = False # Start evaluations # Sequence: From 1292e08fe47ab5ae8455c24f70612708988bd46c Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Apr 2021 11:26:03 +0300 Subject: [PATCH 232/460] Use strategy_safe_wrapper() when calling custom_sell(). --- freqtrade/strategy/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b0710e833..1a007da15 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -557,7 +557,8 @@ class IStrategy(ABC, HyperStrategyMixin): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False elif ask_strategy.get('use_sell_signal', True): - sell = sell or self.custom_sell(trade.pair, trade, date, current_rate, current_profit) + sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) sell_signal = sell and not buy # TODO: return here if sell-signal should be favored over ROI else: From a77337e4241245a8cb77f2c5cf47fe71f3bd5104 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 10:37:45 +0300 Subject: [PATCH 233/460] Document IStrategy.custom_sell. --- docs/strategy-advanced.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 96c927965..758d08fca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -69,6 +69,20 @@ class AwesomeStrategy(IStrategy): See `custom_stoploss` examples below on how to access the saved dataframe columns +## Custom sell signal + +It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. + +An example of how we can sell trades that were open longer than 1 day: + +``` python +class AwesomeStrategy(IStrategy): + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> bool: + time_delta = datetime.datetime.utcnow() - trade.open_date_utc + return time_delta.days >= 1 +``` + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. From 1aad128d85dd2e9962631a1ffd8d5a3fd0dacbd1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 11:17:00 +0300 Subject: [PATCH 234/460] Support returning a string from custom_sell() and have it recorded as custom sell reason. --- freqtrade/freqtradebot.py | 11 ++++--- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/interface.py | 50 ++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ceb822472..9cce8c105 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -961,7 +961,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type) + self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) return True return False @@ -1150,12 +1150,15 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, + custom_reason: Optional[str] = None) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order - :param sellreason: Reason the sell was triggered + :param sell_reason: Reason the sell was triggered + :param custom_reason: A custom sell reason. Provided only if + sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' @@ -1213,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.value + trade.sell_reason = custom_reason or sell_reason.value # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fb9826a23..57ac70cc1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type.value + trade.sell_reason = sell.sell_reason or sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1a007da15..74e92f389 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) +CUSTOM_SELL_MAX_LENGTH = 64 class SignalType(Enum): @@ -45,6 +46,7 @@ class SellType(Enum): SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" EMERGENCY_SELL = "emergency_sell" + CUSTOM_SELL = "custom_sell" NONE = "" def __str__(self): @@ -58,6 +60,7 @@ class SellCheckTuple(NamedTuple): """ sell_flag: bool sell_type: SellType + sell_reason: Optional[str] = None class IStrategy(ABC, HyperStrategyMixin): @@ -286,25 +289,28 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ - Custom sell signal logic indicating that specified position should be sold. Returning True - from this method is equal to setting sell signal on a candle at specified time. - This method is not called when sell signal is set. + Custom sell signal logic indicating that specified position should be sold. Returning a + string or True from this method is equal to setting sell signal on a candle at specified + time. This method is not called when sell signal is set. This method should be overridden to create sell signals that depend on trade parameters. For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. + Custom sell reason max length is 64. Exceeding this limit will raise OperationalException. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: Whether trade should exit now. + :return: To execute sell, return a string with custom sell reason or True. Otherwise return + None or False. """ - return False + return None def informative_pairs(self) -> ListPairsWithTimeframes: """ @@ -552,17 +558,27 @@ class IStrategy(ABC, HyperStrategyMixin): and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) + sell_signal = SellType.NONE + custom_reason = None if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal - sell_signal = False - elif ask_strategy.get('use_sell_signal', True): - sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) - sell_signal = sell and not buy + pass + elif ask_strategy.get('use_sell_signal', True) and not buy: + if sell: + sell_signal = SellType.SELL_SIGNAL + else: + custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) + if custom_reason: + sell_signal = SellType.CUSTOM_SELL + if isinstance(custom_reason, bool): + custom_reason = None + elif isinstance(custom_reason, str): + if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: + raise OperationalException('Custom sell reason returned ' + 'from custom_sell is too long.') # TODO: return here if sell-signal should be favored over ROI - else: - sell_signal = False # Start evaluations # Sequence: @@ -574,10 +590,10 @@ class IStrategy(ABC, HyperStrategyMixin): f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if sell_signal: + if sell_signal != SellType.NONE: 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) + f"sell_type={sell_signal}, custom_reason={custom_reason}") + return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: From a90e7956958c6bcb63101f65c91d442ceaf10c83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 21 Apr 2021 09:37:16 +0300 Subject: [PATCH 235/460] Warn and trim custom sell reason if it is too long. --- freqtrade/strategy/interface.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 74e92f389..435ac3ed3 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -572,12 +572,14 @@ class IStrategy(ABC, HyperStrategyMixin): trade.pair, trade, date, current_rate, current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL - if isinstance(custom_reason, bool): - custom_reason = None - elif isinstance(custom_reason, str): + if isinstance(custom_reason, str): if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: - raise OperationalException('Custom sell reason returned ' - 'from custom_sell is too long.') + logger.warning(f'Custom sell reason returned from custom_sell is too ' + f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' + f'characters.') + custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] + else: + custom_reason = None # TODO: return here if sell-signal should be favored over ROI # Start evaluations From bfad4e82adf1c6413835a8e1e27b8d49f5ae4c5d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 21 Apr 2021 10:11:54 +0300 Subject: [PATCH 236/460] Make execute_sell() use SellCheckTuple for sell reason. --- freqtrade/freqtradebot.py | 20 ++++++++++---------- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/strategy/interface.py | 16 +++++++++++----- tests/test_freqtradebot.py | 22 +++++++++++++--------- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9cce8c105..7888e64bc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -28,7 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State -from freqtrade.strategy.interface import IStrategy, SellType +from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -850,7 +850,8 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') - self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) + self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( + sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None @@ -961,7 +962,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) + self.execute_sell(trade, sell_rate, should_sell) return True return False @@ -1150,8 +1151,7 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, - custom_reason: Optional[str] = None) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance @@ -1162,7 +1162,7 @@ class FreqtradeBot(LoggingMixin): :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' - if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1179,10 +1179,10 @@ class FreqtradeBot(LoggingMixin): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") order_type = self.strategy.order_types[sell_type] - if sell_reason == SellType.EMERGENCY_SELL: + if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason == SellType.FORCE_SELL: + if sell_reason.sell_type == SellType.FORCE_SELL: # Force sells (default to the sell_type defined in the strategy, # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) @@ -1193,7 +1193,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, - sell_reason=sell_reason.value): + sell_reason=sell_reason.sell_type.value): logger.info(f"User requested abortion of selling {trade.pair}") return False @@ -1216,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = custom_reason or sell_reason.value + trade.sell_reason = sell_reason.sell_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4a8907582..957f31b63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,7 +24,7 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.strategy.interface import SellType +from freqtrade.strategy.interface import SellCheckTuple, SellType logger = logging.getLogger(__name__) @@ -554,7 +554,8 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 435ac3ed3..c73574729 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -54,13 +54,18 @@ class SellType(Enum): return self.value -class SellCheckTuple(NamedTuple): +class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool + sell_flag: bool # TODO: Remove? sell_type: SellType - sell_reason: Optional[str] = None + sell_reason: Optional[str] + + def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): + self.sell_flag = sell_flag + self.sell_type = sell_type + self.sell_reason = sell_reason or sell_type.value class IStrategy(ABC, HyperStrategyMixin): @@ -594,7 +599,8 @@ class IStrategy(ABC, HyperStrategyMixin): if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type={sell_signal}, custom_reason={custom_reason}") + f"sell_type=SellType.{sell_signal.name}" + + (f", custom_reason={custom_reason}" if custom_reason else "")) return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ad000515e..aecff95ee 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2606,14 +2606,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N fetch_ticker=ticker_sell_up ) # Prevented sell ... - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2665,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2722,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2774,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2824,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.SELL_SIGNAL) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2929,7 +2931,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee, ) freqtrade.config['order_types']['sell'] = 'market' - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2983,8 +2986,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.ROI) + sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3226,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) From 961b38636fa2ae7c8f4c9a97e5b90c83ff97c232 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 09:21:19 +0300 Subject: [PATCH 237/460] Remove explicit sell_flag parameter from SellCheckTuple. --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/strategy/interface.py | 26 ++++++++++++++------------ tests/test_freqtradebot.py | 24 ++++++++++++------------ tests/test_integration.py | 14 +++++++------- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7888e64bc..08c69bb53 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -851,7 +851,7 @@ class FreqtradeBot(LoggingMixin): logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) + sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 957f31b63..fd97ad7d4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -554,7 +554,7 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c73574729..bc8b3e59f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -58,15 +58,17 @@ class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool # TODO: Remove? sell_type: SellType sell_reason: Optional[str] - def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): - self.sell_flag = sell_flag + def __init__(self, sell_type: SellType, sell_reason: Optional[str] = None): self.sell_type = sell_type self.sell_reason = sell_reason or sell_type.value + @property + def sell_flag(self): + return self.sell_type != SellType.NONE + class IStrategy(ABC, HyperStrategyMixin): """ @@ -593,25 +595,25 @@ class IStrategy(ABC, HyperStrategyMixin): # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " + logger.debug(f"{trade.pair} - Required profit reached. " f"sell_type=SellType.ROI") - return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: - logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " + logger.debug(f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) - return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) + return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + logger.debug(f"{trade.pair} - Stoploss hit. " 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) + # logger.debug(f"{trade.pair} - No sell signal.") + return SellCheckTuple(sell_type=SellType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, @@ -675,9 +677,9 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"{trade.pair} - Trailing stop saved " f"{trade.stop_loss - trade.initial_stop_loss:.6f}") - return SellCheckTuple(sell_flag=True, sell_type=sell_type) + return SellCheckTuple(sell_type=sell_type) - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + return SellCheckTuple(sell_type=SellType.NONE) def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aecff95ee..7a2f6a1ed 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1973,7 +1973,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # if ROI is reached we must sell patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has("ETH/BTC - Required profit reached. sell_flag=True, sell_type=SellType.ROI", + assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) @@ -2002,7 +2002,7 @@ def test_handle_trade_use_sell_signal( patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has("ETH/BTC - Sell signal received. sell_flag=True, sell_type=SellType.SELL_SIGNAL", + assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -2607,7 +2607,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N ) # Prevented sell ... freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -2615,7 +2615,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2667,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2724,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2776,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2826,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2932,7 +2932,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, freqtrade.config['order_types']['sell'] = 'market' freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2986,7 +2986,7 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) - sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + sell_reason = SellCheckTuple(sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3081,7 +3081,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_flag=False, sell_type=SellType.NONE)) + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() @@ -3230,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) diff --git a/tests/test_integration.py b/tests/test_integration.py index be0dd1137..217910961 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -51,8 +51,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) # Sell 3rd trade (not called for the first trade) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.SELL_SIGNAL)] ) cancel_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) @@ -156,11 +156,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc _notify_sell=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL), - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)] + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.SELL_SIGNAL), + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.NONE)] ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) From 595b8735f80df633834a4d8266694cdcb52287b8 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:15:56 +0300 Subject: [PATCH 238/460] Add dataframe parameter to custom_stoploss() and custom_sell() methods. --- freqtrade/freqtradebot.py | 13 +++++++------ freqtrade/optimize/backtesting.py | 7 ++++--- freqtrade/strategy/interface.py | 21 ++++++++++++--------- tests/strategy/test_default_strategy.py | 3 ++- tests/strategy/test_interface.py | 4 ++-- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 08c69bb53..f8c757189 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache +from pandas import DataFrame from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -783,10 +784,10 @@ class FreqtradeBot(LoggingMixin): config_ask_strategy = self.config.get('ask_strategy', {}) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) @@ -813,13 +814,13 @@ class FreqtradeBot(LoggingMixin): # resulting in outdated RPC messages self._sell_rate_cache[trade.pair] = sell_rate - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -950,13 +951,13 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: """ Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.now(timezone.utc), buy, sell, + dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 57ac70cc1..559938e9e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -247,9 +247,10 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade, + sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore + sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) @@ -396,7 +397,7 @@ class Backtesting: for trade in open_trades[pair]: # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(trade, row) + trade_entry = self._get_sell_trade_entry(processed[pair], trade, row) # Sell occured if trade_entry: # logger.debug(f"{pair} - Backtesting sell {trade}") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index bc8b3e59f..9a6901712 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -274,7 +274,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, dataframe: DataFrame, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -296,7 +296,8 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> Optional[Union[str, bool]]: + current_profit: float, dataframe: DataFrame, + **kwargs) -> Optional[Union[str, bool]]: """ Custom sell signal logic indicating that specified position should be sold. Returning a string or True from this method is equal to setting sell signal on a candle at specified @@ -534,8 +535,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, - sell: bool, low: float = None, high: float = None, + def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, + buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell @@ -551,8 +552,9 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, + stoplossflag = self.stop_loss_reached(dataframe=dataframe, 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 @@ -576,7 +578,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) + trade.pair, trade, date, current_rate, current_profit, dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -615,7 +617,7 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def stop_loss_reached(self, current_rate: float, trade: Trade, + def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ @@ -633,7 +635,8 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, - current_profit=current_profit) + current_profit=current_profit, + dataframe=dataframe) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index ec7b3c33d..a8862e9c9 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -41,4 +41,5 @@ def test_default_strategy(result, fee): rate=20000, time_in_force='gtc', sell_reason='roi') is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05) == strategy.stoploss + current_rate=20_000, current_profit=0.05, dataframe=None + ) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 347d35b19..d2a09e466 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_time=now, current_profit=profit, - force_stoploss=0, high=None) + force_stoploss=0, high=None, dataframe=None) assert isinstance(sl_flag, SellCheckTuple) assert sl_flag.sell_type == expected if expected == SellType.NONE: @@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, current_time=now, current_profit=profit2, - force_stoploss=0, high=None) + force_stoploss=0, high=None, dataframe=None) assert sl_flag.sell_type == expected2 if expected2 == SellType.NONE: assert sl_flag.sell_flag is False From 004550529ea498fc584668d8b292e2fcab7d37e1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:31:53 +0300 Subject: [PATCH 239/460] Document dataframe parameter in custom_stoploss(). --- docs/strategy-advanced.md | 83 ++++++++++++++------------------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 758d08fca..7648498d3 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -42,33 +42,6 @@ class AwesomeStrategy(IStrategy): *** -### Storing custom information using DatetimeIndex from `dataframe` - -Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. - -```python -import talib.abstract as ta -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # using "ATR" here as example - dataframe['atr'] = ta.ATR(dataframe) - if self.dp.runmode.value in ('backtest', 'hyperopt'): - # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') - return dataframe -``` - -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. - -!!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - -See `custom_stoploss` examples below on how to access the saved dataframe columns - ## Custom sell signal It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. @@ -107,7 +80,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: Dataframe, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -157,7 +131,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -183,7 +158,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -209,7 +185,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -249,7 +226,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -266,14 +244,20 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` - !!! Warning - only use .iat[-1] in live mode, not in backtesting/hyperopt - otherwise you will look into the future + Only use `dataframe` values up until and including `current_time` value. Reading past + `current_time` you will look into the future, which will produce incorrect backtesting results + and throw an exception in dry/live runs. see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. +!!! Note + DataFrame is indexed by candle date. During dry/live runs `current_time` and + `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw + an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous + candle before using it as a `dataframe` index. + ``` python +from freqtrade.exchange import timeframe_to_prev_date from freqtrade.persistence import Trade from freqtrade.state import RunMode @@ -284,28 +268,19 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: result = 1 if self.custom_info and pair in self.custom_info and trade: - # using current_time directly (like below) will only work in backtesting. - # so check "runmode" to make sure that it's only used in backtesting/hyperopt - if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): - relative_sl = self.custom_info[pair].loc[current_time]['atr'] - # in live / dry-run, it'll be really the current time - else: - # but we can just use the last entry from an already analyzed dataframe instead - dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, - timeframe=self.timeframe) - # WARNING - # only use .iat[-1] in live mode, not in backtesting/hyperopt - # otherwise you will look into the future - # see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies - relative_sl = dataframe['atr'].iat[-1] - - if (relative_sl is not None): + # Using current_time directly would only work in backtesting. Live/dry runs need time to + # be rounded to previous candle to be used as dataframe index. Rounding must also be + # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. + current_time = timeframe_to_prev_date(self.timeframe, current_time) + current_row = dataframe.loc[current_time] + if 'atr' in current_row: # new stoploss relative to current_rate - new_stoploss = (current_rate-relative_sl)/current_rate + new_stoploss = (current_rate - current_row['atr']) / current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 From e58fe7a8cbbb978aa7c7a8ffcec4ebe0e3660414 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:45:34 +0300 Subject: [PATCH 240/460] Update custom_sell documentation. --- docs/strategy-advanced.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7648498d3..59c4a6a35 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,16 +46,31 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. -An example of how we can sell trades that were open longer than 1 day: +An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: ``` python class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: - time_delta = datetime.datetime.utcnow() - trade.open_date_utc - return time_delta.days >= 1 + current_profit: float, dataframe: Dataframe, **kwargs) -> bool: + trade_row = dataframe.loc[timeframe_to_prev_date(trade.open_date_utc)] + + # Sell when price falls below value in stoploss column of taken buy signal. + if 'stop_loss' in trade_row: + if current_rate <= trade_row['stop_loss'] < trade.open_rate: + return 'stop_loss' + + # Sell when price reaches value in take_profit column of taken buy signal. + if 'take_profit' in trade_row: + if trade.open_rate < trade_row['take_profit'] <= current_rate: + return 'take_profit' + + # Sell any positions at a loss if they are helpd for more than two days. + if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: + return 'unclog' ``` +See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. From 98f6fce2ecf6decd360d5706d1c6ffca38643b5d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:48:24 +0300 Subject: [PATCH 241/460] Use correct sell reason in case of custom sell reason. --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f8c757189..6dcd920c7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1194,7 +1194,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, - sell_reason=sell_reason.sell_type.value): + sell_reason=sell_reason.sell_reason): logger.info(f"User requested abortion of selling {trade.pair}") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 559938e9e..88957bafb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -256,7 +256,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_reason or sell.sell_type.value + trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -266,7 +266,7 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_type.value): + sell_reason=sell.sell_reason): return None trade.close(closerate, show_msg=False) From fd3afdc230edb38db7ea92091259af537e1ed4bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 10:10:09 +0200 Subject: [PATCH 242/460] plot-profit should use absolute values --- freqtrade/data/btanalysis.py | 6 +++--- freqtrade/plot/plotting.py | 8 ++++---- tests/data/test_btanalysis.py | 5 +++-- tests/test_plotting.py | 9 +++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c98477f4e..e07b0a40f 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -337,7 +337,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index - :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + :param trades: DataFrame containing trades (requires columns close_date and profit_abs) :param col_name: Column name that will be assigned the results :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. @@ -349,8 +349,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, timeframe_minutes = timeframe_to_minutes(timeframe) # Resample to timeframe to make sure trades match candles _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' - )[['profit_ratio']].sum() - df.loc[:, col_name] = _trades_sum['profit_ratio'].cumsum() + )[['profit_abs']].sum() + df.loc[:, col_name] = _trades_sum['profit_abs'].cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 # FFill to get continuous diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 682c2b018..5ffe7f155 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -441,7 +441,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], - trades: pd.DataFrame, timeframe: str) -> go.Figure: + trades: pd.DataFrame, timeframe: str, stake_currency: str) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_dataframes_with_mean(data, "close") @@ -466,8 +466,8 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], subplot_titles=["AVG Close Price", "Combined Profit", "Profit per pair"]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title='Profit') - fig['layout']['yaxis3'].update(title='Profit') + fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}') + fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}') fig['layout']['xaxis']['rangeslider'].update(visible=False) fig.add_trace(avgclose, 1, 1) @@ -581,6 +581,6 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], - trades, config.get('timeframe', '5m')) + trades, config.get('timeframe', '5m'), config.get('stake_currency')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', auto_open=True) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index e42c13e18..6bde60926 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,3 +1,4 @@ +from math import isclose from pathlib import Path from unittest.mock import MagicMock @@ -246,7 +247,7 @@ def test_create_cum_profit(testdatadir): "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 - assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + assert isclose(cum_profits.iloc[-1]['cum_profits'], 8.723007518796964e-06) def test_create_cum_profit1(testdatadir): @@ -264,7 +265,7 @@ def test_create_cum_profit1(testdatadir): "cum_profits", timeframe="5m") assert "cum_profits" in cum_profits.columns assert cum_profits.iloc[0]['cum_profits'] == 0 - assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 + assert isclose(cum_profits.iloc[-1]['cum_profits'], 8.723007518796964e-06) with pytest.raises(ValueError, match='Trade dataframe empty.'): create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'], diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 1752f9b94..a22c8c681 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -331,13 +331,13 @@ def test_generate_profit_graph(testdatadir): trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, data, trades, timeframe="5m") + fig = generate_profit_graph(pairs, data, trades, timeframe="5m", stake_currency='BTC') assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" assert fig.layout.yaxis.title.text == "Price" - assert fig.layout.yaxis2.title.text == "Profit" - assert fig.layout.yaxis3.title.text == "Profit" + assert fig.layout.yaxis2.title.text == "Profit BTC" + assert fig.layout.yaxis3.title.text == "Profit BTC" figure = fig.layout.figure assert len(figure.data) == 5 @@ -356,7 +356,8 @@ def test_generate_profit_graph(testdatadir): with pytest.raises(OperationalException, match=r"No trades found.*"): # Pair cannot be empty - so it's an empty dataframe. - generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m") + generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m", + stake_currency='BTC') def test_start_plot_dataframe(mocker): From 7448a05f15db0b0ad5c4e023cbd2ea51d500f9ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 11:01:04 +0200 Subject: [PATCH 243/460] Use correct variable in pairlist_manager --- freqtrade/plot/plotting.py | 3 ++- freqtrade/plugins/pairlistmanager.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5ffe7f155..d5a729ee1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -581,6 +581,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], - trades, config.get('timeframe', '5m'), config.get('stake_currency')) + trades, config.get('timeframe', '5m'), + config.get('stake_currency', '')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', auto_open=True) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 4e4135981..29c78084c 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -83,7 +83,7 @@ class PairListManager(): pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers) # Generate the pairlist with first Pairlist Handler in the chain - pairlist = self._pairlist_handlers[0].gen_pairlist(self._whitelist, tickers) + pairlist = self._pairlist_handlers[0].gen_pairlist(pairlist, tickers) # Process all Pairlist Handlers in the chain for pairlist_handler in self._pairlist_handlers: From 9c21c75cf5ef34cdd59d340e8e0b1e3f7b504ba0 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 13:12:33 +0300 Subject: [PATCH 244/460] Fix inaccuracy in docs. --- docs/strategy-advanced.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 59c4a6a35..f869a3c3a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -52,8 +52,8 @@ An example of how we can set stop-loss and take-profit targets in the dataframe class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, dataframe: Dataframe, **kwargs) -> bool: - trade_row = dataframe.loc[timeframe_to_prev_date(trade.open_date_utc)] - + trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze() + # Sell when price falls below value in stoploss column of taken buy signal. if 'stop_loss' in trade_row: if current_rate <= trade_row['stop_loss'] < trade.open_rate: @@ -292,7 +292,7 @@ class AwesomeStrategy(IStrategy): # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. current_time = timeframe_to_prev_date(self.timeframe, current_time) - current_row = dataframe.loc[current_time] + current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() if 'atr' in current_row: # new stoploss relative to current_rate new_stoploss = (current_rate - current_row['atr']) / current_rate From bb7ef2f8044a8b8a70ae1a47445a1e84f8f18705 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 20:10:47 +0200 Subject: [PATCH 245/460] Cache pairlist in pairlist, not globally closes #4797 closes #4689 --- freqtrade/plugins/pairlist/IPairList.py | 3 +-- freqtrade/plugins/pairlist/StaticPairList.py | 3 +-- freqtrade/plugins/pairlist/VolumePairList.py | 22 ++++++++++++-------- freqtrade/plugins/pairlistmanager.py | 20 ++---------------- tests/plugins/test_pairlist.py | 7 ++----- 5 files changed, 19 insertions(+), 36 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index c4a9c3e40..3e6252fc4 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -73,7 +73,7 @@ class IPairList(LoggingMixin, ABC): """ raise NotImplementedError() - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + def gen_pairlist(self, tickers: Dict) -> List[str]: """ Generate the pairlist. @@ -84,7 +84,6 @@ class IPairList(LoggingMixin, ABC): it will raise the exception if a Pairlist Handler is used at the first position in the chain. - :param cached_pairlist: Previously generated pairlist (cached) :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index 13d30fc47..d8623e13d 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -42,10 +42,9 @@ class StaticPairList(IPairList): """ return f"{self.name}" - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + def gen_pairlist(self, tickers: Dict) -> List[str]: """ Generate the pairlist - :param cached_pairlist: Previously generated pairlist (cached) :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index e85fb1805..8eff137b0 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -4,9 +4,10 @@ Volume PairList provider Provides dynamic pair list based on trade volumes """ import logging -from datetime import datetime from typing import Any, Dict, List +from cachetools.ttl import TTLCache + from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -33,7 +34,8 @@ class VolumePairList(IPairList): self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') self._min_value = self._pairlistconfig.get('min_value', 0) - self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) + self._refresh_period = self._pairlistconfig.get('refresh_period', 1800) + self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) if not self._exchange.exchange_has('fetchTickers'): raise OperationalException( @@ -63,17 +65,19 @@ class VolumePairList(IPairList): """ return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs." - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + def gen_pairlist(self, tickers: Dict) -> List[str]: """ Generate the pairlist - :param cached_pairlist: Previously generated pairlist (cached) :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ # Generate dynamic whitelist # Must always run if this pairlist is not the first in the list. - if self._last_refresh + self.refresh_period < datetime.now().timestamp(): - self._last_refresh = int(datetime.now().timestamp()) + pairlist = self._pair_cache.get('pairlist') + if pairlist: + # Item found - no refresh necessary + return pairlist + else: # Use fresh pairlist # Check if pair quote currency equals to the stake currency. @@ -82,9 +86,9 @@ class VolumePairList(IPairList): if (self._exchange.get_pair_quote_currency(k) == self._stake_currency and v[self._sort_key] is not None)] pairlist = [s['symbol'] for s in filtered_tickers] - else: - # Use the cached pairlist if it's not time yet to refresh - pairlist = cached_pairlist + + pairlist = self.filter_pairlist(pairlist, tickers) + self._pair_cache['pairlist'] = pairlist return pairlist diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 29c78084c..d1cdd2c5b 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -3,7 +3,7 @@ PairList manager class """ import logging from copy import deepcopy -from typing import Any, Dict, List +from typing import Dict, List from cachetools import TTLCache, cached @@ -79,11 +79,8 @@ class PairListManager(): if self._tickers_needed: tickers = self._get_cached_tickers() - # Adjust whitelist if filters are using tickers - pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers) - # Generate the pairlist with first Pairlist Handler in the chain - pairlist = self._pairlist_handlers[0].gen_pairlist(pairlist, tickers) + pairlist = self._pairlist_handlers[0].gen_pairlist(tickers) # Process all Pairlist Handlers in the chain for pairlist_handler in self._pairlist_handlers: @@ -95,19 +92,6 @@ class PairListManager(): self._whitelist = pairlist - def _prepare_whitelist(self, pairlist: List[str], tickers: Dict[str, Any]) -> List[str]: - """ - Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove - pairs that do not have ticker available - """ - if self._tickers_needed: - # Copy list since we're modifying this list - for p in deepcopy(pairlist): - if p not in tickers: - pairlist.remove(p) - - return pairlist - def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]: """ Verify and remove items from pairlist - returning a filtered pairlist. diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index bf225271f..57bbbdaf5 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -604,17 +604,14 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): get_tickers=tickers ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == 0 + assert len(freqtrade.pairlists._pairlist_handlers[0]._pair_cache) == 0 assert tickers.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert tickers.call_count == 1 - assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh != 0 - lrf = freqtrade.pairlists._pairlist_handlers[0]._last_refresh + assert len(freqtrade.pairlists._pairlist_handlers[0]._pair_cache) == 1 freqtrade.pairlists.refresh_pairlist() assert tickers.call_count == 1 - # Time should not be updated. - assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): From 4a5eba3db476b0fa10d846372e387441f4d7ad17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:25:53 +0000 Subject: [PATCH 246/460] Bump mkdocs-material from 7.1.2 to 7.1.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.2 to 7.1.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.2...7.1.3) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 4d7082a7f..a431b1702 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.2 +mkdocs-material==7.1.3 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 14ef080d2886017d7b71fb73430e1cea750f332f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:02 +0000 Subject: [PATCH 247/460] Bump sqlalchemy from 1.4.9 to 1.4.11 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.9 to 1.4.11. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a89eb2383..08b1fef0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.48.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.9 +SQLAlchemy==1.4.11 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 09a3448fd4967c4a1ac33175698911e28af0ef44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:09 +0000 Subject: [PATCH 248/460] Bump pytest-mock from 3.5.1 to 3.6.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.5.1 to 3.6.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.5.1...v3.6.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 5e96e2cc2..e3d6ddca8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.812 pytest==6.2.3 pytest-asyncio==0.15.0 pytest-cov==2.11.1 -pytest-mock==3.5.1 +pytest-mock==3.6.0 pytest-random-order==1.0.4 isort==5.8.0 From e5bdafd4ab71214665e34ae5ba8cb5baad42784d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:37 +0000 Subject: [PATCH 249/460] Bump ccxt from 1.48.22 to 1.48.76 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.48.22 to 1.48.76. - [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.48.22...1.48.76) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a89eb2383..b8f0ace87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.48.22 +ccxt==1.48.76 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From bdcf21e18728bf42f232085653ad982ff98d13c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:43 +0000 Subject: [PATCH 250/460] Bump pycoingecko from 1.4.1 to 2.0.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.4.1 to 2.0.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.4.1...2.0.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a89eb2383..bb5a5c577 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.19 technical==1.2.2 tabulate==0.8.9 -pycoingecko==1.4.1 +pycoingecko==2.0.0 jinja2==2.11.3 tables==3.6.1 blosc==1.10.2 From 02160d52e3fc5a29c15550aa24169417cb6f98c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 05:26:59 +0000 Subject: [PATCH 251/460] Bump scipy from 1.6.2 to 1.6.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.2...v1.6.3) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 9eb490f83..79ad722e2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.2 +scipy==1.6.3 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 From 31a2285eacaa7ce011a98548809fbe314f4f3c79 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 26 Apr 2021 10:42:24 +0300 Subject: [PATCH 252/460] Fix mypy complaints. --- freqtrade/strategy/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9a6901712..503a587ea 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -59,9 +59,9 @@ class SellCheckTuple(object): NamedTuple for Sell type + reason """ sell_type: SellType - sell_reason: Optional[str] + sell_reason: str = '' - def __init__(self, sell_type: SellType, sell_reason: Optional[str] = None): + def __init__(self, sell_type: SellType, sell_reason: str = ''): self.sell_type = sell_type self.sell_reason = sell_reason or sell_type.value @@ -568,7 +568,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=date)) sell_signal = SellType.NONE - custom_reason = None + custom_reason = '' if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal From 6f0a585bd0d68b75342e24bc273748b9f209d513 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 11:53:33 +0200 Subject: [PATCH 253/460] Fix random test failure due to ttl 0 issue --- tests/plugins/test_pairlist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 57bbbdaf5..8b060c287 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring,C0103,protected-access +import time from unittest.mock import MagicMock, PropertyMock import pytest @@ -260,6 +261,8 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ freqtrade.pairlists.refresh_pairlist() assert whitelist == freqtrade.pairlists.whitelist + # Delay to allow 0 TTL cache to expire... + time.sleep(1) whitelist = ['FUEL/BTC', 'ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC'] tickers_dict['FUEL/BTC']['quoteVolume'] = 10000.0 freqtrade.pairlists.refresh_pairlist() From 298f54adff77bd408a9b7cd545a9333894e7ec7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Apr 2021 10:47:09 +0000 Subject: [PATCH 254/460] Bump pytest-asyncio from 0.15.0 to 0.15.1 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.15.0 to 0.15.1. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.15.0...v0.15.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e3d6ddca8..adf5a81c3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 pytest==6.2.3 -pytest-asyncio==0.15.0 +pytest-asyncio==0.15.1 pytest-cov==2.11.1 pytest-mock==3.6.0 pytest-random-order==1.0.4 From 3f84c37a79d4ef5f77656903898f31773bffb6f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 14:12:52 +0200 Subject: [PATCH 255/460] Fix wallet calls closes #4810 #4812 --- freqtrade/wallets.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index bbbe5ba5e..d5f979c24 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -99,12 +99,13 @@ class Wallets: balances = self._exchange.get_balances() for currency in balances: - self._wallets[currency] = Wallet( - currency, - balances[currency].get('free', None), - balances[currency].get('used', None), - balances[currency].get('total', None) - ) + if isinstance(balances[currency], dict): + self._wallets[currency] = Wallet( + currency, + balances[currency].get('free', None), + balances[currency].get('used', None), + balances[currency].get('total', None) + ) # Remove currencies no longer in get_balances output for currency in deepcopy(self._wallets): if currency not in balances: From dbf33271b5853d32d4bd952508ce33be2574948c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 19:27:22 +0200 Subject: [PATCH 256/460] Small doc changes --- docs/strategy-advanced.md | 24 +++++++++++-------- docs/strategy-customization.md | 5 ++-- freqtrade/freqtradebot.py | 2 -- freqtrade/strategy/interface.py | 1 + .../subtemplates/strategy_methods_advanced.j2 | 6 +++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f869a3c3a..f702b1448 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -49,10 +49,13 @@ It is possible to define custom sell signals. This is very useful when we need t An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: ``` python +from freqtrade.strategy import IStrategy, timeframe_to_prev_date + class AwesomeStrategy(IStrategy): - def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: Dataframe, **kwargs) -> bool: - trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze() + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, dataframe: DataFrame, **kwargs): + trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) + trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() # Sell when price falls below value in stoploss column of taken buy signal. if 'stop_loss' in trade_row: @@ -64,12 +67,12 @@ class AwesomeStrategy(IStrategy): if trade.open_rate < trade_row['take_profit'] <= current_rate: return 'take_profit' - # Sell any positions at a loss if they are helpd for more than two days. + # Sell any positions at a loss if they are held for more than two days. if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. ## Custom stoploss @@ -95,7 +98,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: Dataframe, + current_rate: float, current_profit: float, dataframe: DataFrame, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). @@ -113,7 +116,7 @@ class AwesomeStrategy(IStrategy): :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the currentrate + :return float: New stoploss value, relative to the current rate """ return -0.04 ``` @@ -228,7 +231,6 @@ Instead of continuously trailing behind the current price, this example sets fix * Once profit is > 25% - set stoploss to 15% above open price. * Once profit is > 40% - set stoploss to 25% above open price. - ``` python from datetime import datetime from freqtrade.persistence import Trade @@ -255,6 +257,7 @@ class AwesomeStrategy(IStrategy): # return maximum stoploss value, keeping current stoploss price unchanged return 1 ``` + #### Custom stoploss using an indicator from dataframe example Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" @@ -266,7 +269,7 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. !!! Note - DataFrame is indexed by candle date. During dry/live runs `current_time` and + `dataframe` is indexed by candle date. During dry/live runs `current_time` and `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous candle before using it as a `dataframe` index. @@ -286,8 +289,9 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, dataframe: DataFrame, **kwargs) -> float: + # Default return value result = 1 - if self.custom_info and pair in self.custom_info and trade: + if trade: # Using current_time directly would only work in backtesting. Live/dry runs need time to # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 256b28990..59bfbde48 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -631,9 +631,10 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: - # once the profit has risin above 10%, keep the stoploss at 7% above the open price + # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: return stoploss_from_open(0.07, current_profit) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6dcd920c7..c2b15d23f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1158,8 +1158,6 @@ class FreqtradeBot(LoggingMixin): :param trade: Trade instance :param limit: limit rate for the sell order :param sell_reason: Reason the sell was triggered - :param custom_reason: A custom sell reason. Provided only if - sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 503a587ea..5c3264c35 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -290,6 +290,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 53ededa19..c69b52cad 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -14,8 +14,9 @@ def bot_loop_start(self, **kwargs) -> None: use_custom_stoploss = True -def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> float: +def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -31,6 +32,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ From 2061162d794fa53eb847dfbf2811282e5825146a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 19:54:47 +0200 Subject: [PATCH 257/460] Convert trade-opendate to python datetime --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 88957bafb..b03a459ea 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade.close_date = sell_row[DATE_IDX] + trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -294,7 +294,7 @@ class Backtesting: trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], - open_date=row[DATE_IDX], + open_date=row[DATE_IDX].to_pydatetime(), stake_amount=stake_amount, amount=round(stake_amount / row[OPEN_IDX], 8), fee_open=self.fee, @@ -316,7 +316,7 @@ class Backtesting: for trade in open_trades[pair]: sell_row = data[pair][-1] - trade.close_date = sell_row[DATE_IDX] + trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) From 55faa6a84a6ed7a4fa65781987eb9f05d8de4152 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 20:18:03 +0200 Subject: [PATCH 258/460] safe_wrapper should use kwargs to call methods --- freqtrade/strategy/interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5c3264c35..645e70e8a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -579,7 +579,8 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit, dataframe) + pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, + current_profit=current_profit, dataframe=dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -598,8 +599,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. " - f"sell_type=SellType.ROI") + logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: @@ -610,8 +610,7 @@ class IStrategy(ABC, HyperStrategyMixin): if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. " - f"sell_type={stoplossflag.sell_type}") + logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}") return stoplossflag # This one is noisy, commented out... From cc916ab2e90458a54cdc8d4f247518f904e8fdd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 20:26:14 +0200 Subject: [PATCH 259/460] Add test for custom_sell --- tests/strategy/test_interface.py | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d2a09e466..bd81bc80c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -382,6 +382,50 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.custom_stoploss = original_stopvalue +def test_custom_sell(default_conf, fee, caplog) -> None: + + default_conf.update({'strategy': 'DefaultStrategy'}) + + strategy = StrategyResolver.load_strategy(default_conf) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.01, + amount=1, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + ) + + now = arrow.utcnow().datetime + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + + assert res.sell_flag is False + assert res.sell_type == SellType.NONE + + strategy.custom_sell = MagicMock(return_value=True) + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_flag is True + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_reason == 'custom_sell' + + strategy.custom_sell = MagicMock(return_value='hello world') + + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_flag is True + assert res.sell_reason == 'hello world' + + caplog.clear() + strategy.custom_sell = MagicMock(return_value='h' * 100) + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_flag is True + assert res.sell_reason == 'h' * 64 + assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 1465af50d78730aeacb71ef26c87ef5674531940 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Apr 2021 19:17:49 +0200 Subject: [PATCH 260/460] FTX usable configuration --- config_ftx.json.example | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/config_ftx.json.example b/config_ftx.json.example index 33e884976..facd54b25 100644 --- a/config_ftx.json.example +++ b/config_ftx.json.example @@ -1,7 +1,7 @@ { "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.05, + "stake_currency": "USD", + "stake_amount": 50, "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "timeframe": "5m", @@ -38,24 +38,24 @@ "rateLimit": 50 }, "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" + "BTC/USD", + "ETH/USD", + "BNB/USD", + "USDT/USD", + "LTC/USD", + "SRM/USD", + "SXP/USD", + "XRP/USD", + "DOGE/USD", + "1INCH/USD", + "CHZ/USD", + "MATIC/USD", + "LINK/USD", + "OXY/USD", + "SUSHI/USD" ], "pair_blacklist": [ - "BNB/BTC" + "FTT/USD" ] }, "pairlists": [ From 6eb947ae09416acac11c2c4b79f342d24986993b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Apr 2021 20:25:36 +0200 Subject: [PATCH 261/460] Move static Trade functions to right class --- freqtrade/persistence/models.py | 198 +++++++++++++++++--------------- tests/test_persistence.py | 33 +++++- 2 files changed, 132 insertions(+), 99 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e7fd488c7..afd51366a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -567,23 +567,6 @@ class LocalTrade(): else: return None - @staticmethod - def get_trades(trade_filter=None) -> Query: - """ - Helper function to query Trades using filters. - :param trade_filter: Optional filter to apply to trades - Can be either a Filter object, or a List of filters - e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` - e.g. `(trade_filter=Trade.id == trade_id)` - :return: unsorted query object - """ - if trade_filter is not None: - if not isinstance(trade_filter, list): - trade_filter = [trade_filter] - return Trade.query.filter(*trade_filter) - else: - return Trade.query - @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, @@ -636,83 +619,7 @@ class LocalTrade(): """ Query trades from persistence layer """ - return Trade.get_trades(Trade.is_open.is_(True)).all() - - @staticmethod - def get_open_order_trades(): - """ - Returns all open trades - """ - return Trade.get_trades(Trade.open_order_id.isnot(None)).all() - - @staticmethod - def get_open_trades_without_assigned_fees(): - """ - Returns all open trades which don't have open fees set correctly - """ - return Trade.get_trades([Trade.fee_open_currency.is_(None), - Trade.orders.any(), - Trade.is_open.is_(True), - ]).all() - - @staticmethod - def get_sold_trades_without_assigned_fees(): - """ - Returns all closed trades which don't have fees set correctly - """ - return Trade.get_trades([Trade.fee_close_currency.is_(None), - Trade.orders.any(), - Trade.is_open.is_(False), - ]).all() - - @staticmethod - def total_open_trades_stakes() -> float: - """ - Calculates total invested amount in open trades - in stake currency - """ - if Trade.use_db: - total_open_stake_amount = Trade.query.with_entities( - func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() - else: - total_open_stake_amount = sum( - t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) - return total_open_stake_amount or 0 - - @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: - """ - Returns List of dicts containing all Trades, including profit and trade count - """ - pair_rates = Trade.query.with_entities( - Trade.pair, - func.sum(Trade.close_profit).label('profit_sum'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum')) \ - .all() - return [ - { - 'pair': pair, - 'profit': rate, - 'count': count - } - for pair, rate, count in pair_rates - ] - - @staticmethod - def get_best_pair(): - """ - Get best pair with closed trade. - :returns: Tuple containing (pair, profit_sum) - """ - best_pair = Trade.query.with_entities( - Trade.pair, func.sum(Trade.close_profit).label('profit_sum') - ).filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum')).first() - return best_pair + return Trade.get_trades_proxy(is_open=True) @staticmethod def stoploss_reinitialization(desired_stoploss): @@ -810,7 +717,7 @@ class Trade(_DECL_BASE, LocalTrade): open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: """ - Helper function to query Trades. + Helper function to query Trades.j 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. @@ -835,6 +742,107 @@ class Trade(_DECL_BASE, LocalTrade): close_date=close_date ) + @staticmethod + def get_trades(trade_filter=None) -> Query: + """ + Helper function to query Trades using filters. + NOTE: Not supported in Backtesting. + :param trade_filter: Optional filter to apply to trades + Can be either a Filter object, or a List of filters + e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` + e.g. `(trade_filter=Trade.id == trade_id)` + :return: unsorted query object + """ + if not Trade.use_db: + raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.') + if trade_filter is not None: + if not isinstance(trade_filter, list): + trade_filter = [trade_filter] + return Trade.query.filter(*trade_filter) + else: + return Trade.query + + @staticmethod + def get_open_order_trades(): + """ + Returns all open trades + NOTE: Not supported in Backtesting. + """ + return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + + @staticmethod + def get_open_trades_without_assigned_fees(): + """ + Returns all open trades which don't have open fees set correctly + NOTE: Not supported in Backtesting. + """ + return Trade.get_trades([Trade.fee_open_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(True), + ]).all() + + @staticmethod + def get_sold_trades_without_assigned_fees(): + """ + Returns all closed trades which don't have fees set correctly + NOTE: Not supported in Backtesting. + """ + return Trade.get_trades([Trade.fee_close_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(False), + ]).all() + + @staticmethod + def total_open_trades_stakes() -> float: + """ + Calculates total invested amount in open trades + in stake currency + """ + if Trade.use_db: + total_open_stake_amount = Trade.query.with_entities( + func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() + else: + total_open_stake_amount = sum( + t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) + return total_open_stake_amount or 0 + + @staticmethod + def get_overall_performance() -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, including profit and trade count + NOTE: Not supported in Backtesting. + """ + pair_rates = Trade.query.with_entities( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')) \ + .all() + return [ + { + 'pair': pair, + 'profit': rate, + 'count': count + } + for pair, rate, count in pair_rates + ] + + @staticmethod + def get_best_pair(): + """ + Get best pair with closed trade. + NOTE: Not supported in Backtesting. + :returns: Tuple containing (pair, profit_sum) + """ + best_pair = Trade.query.with_entities( + Trade.pair, func.sum(Trade.close_profit).label('profit_sum') + ).filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum')).first() + return best_pair + class PairLock(_DECL_BASE): """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index dad0e275e..8470e12c2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -774,11 +774,16 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") -def test_get_open(fee): +@pytest.mark.parametrize('use_db', [True, False]) +def test_get_open(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() - create_mock_trades(fee) + create_mock_trades(fee, use_db) assert len(Trade.get_open_trades()) == 4 + Trade.use_db = True + @pytest.mark.usefixtures("init_persistence") def test_to_json(default_conf, fee): @@ -1083,6 +1088,13 @@ def test_get_trades_proxy(fee, use_db): Trade.use_db = True +def test_get_trades_backtest(): + Trade.use_db = False + with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"): + Trade.get_trades([]) + Trade.use_db = True + + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): @@ -1216,11 +1228,24 @@ def test_Trade_object_idem(): trade = vars(Trade) localtrade = vars(LocalTrade) + excludes = ( + 'delete', + 'session', + 'query', + 'open_date', + 'get_best_pair', + 'get_overall_performance', + 'total_open_trades_stakes', + 'get_sold_trades_without_assigned_fees', + 'get_open_trades_without_assigned_fees', + 'get_open_order_trades', + 'get_trades', + ) + # Parent (LocalTrade) should have the same attributes for item in trade: # Exclude private attributes and open_date (as it's not assigned a default) - if (not item.startswith('_') - and item not in ('delete', 'session', 'query', 'open_date')): + if (not item.startswith('_') and item not in excludes): assert item in localtrade # Fails if only a column is added without corresponding parent field From 63c28b651982892e0ad3e1a26766090b7886f71a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 16:00:12 +0200 Subject: [PATCH 262/460] Remove obsolete get_balance method --- freqtrade/exchange/exchange.py | 11 ----------- tests/exchange/test_exchange.py | 23 ----------------------- tests/test_freqtradebot.py | 16 +--------------- 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 80b392d73..f61f89fcb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -666,17 +666,6 @@ class Exchange: raise OperationalException(f"stoploss is not implemented for {self.name}.") - @retrier - def get_balance(self, currency: str) -> float: - - # ccxt exception is already handled by get_balances - balances = self.get_balances() - balance = balances.get(currency) - if balance is None: - raise TemporaryError( - f'Could not get {currency} balance due to malformed exchange response: {balances}') - return balance['free'] - @retrier def get_balances(self) -> dict: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 27f4d0db9..573f41bda 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1248,29 +1248,6 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert "timeInForce" not in api_mock.create_order.call_args[0][5] -@pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_balance_prod(default_conf, mocker, exchange_name): - api_mock = MagicMock() - api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4, 'total': 123.4}}) - default_conf['dry_run'] = False - - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - - assert exchange.get_balance(currency='BTC') == 123.4 - - with pytest.raises(OperationalException): - api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - - exchange.get_balance(currency='BTC') - - with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'): - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Kraken.get_balances', MagicMock(return_value={})) - exchange.get_balance(currency='BTC') - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balances_prod(default_conf, mocker, exchange_name): balance_item = { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ad000515e..2c6e86cf3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -82,10 +82,6 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) - ) conf = default_conf.copy() conf['runmode'] = RunMode.DRY_RUN conf['order_types'] = { @@ -117,10 +113,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: def test_order_dict_live(default_conf, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) - ) + conf = default_conf.copy() conf['runmode'] = RunMode.LIVE conf['order_types'] = { @@ -153,10 +146,6 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None: def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) - ) freqtrade = FreqtradeBot(default_conf) @@ -181,7 +170,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2), buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee ) @@ -435,7 +423,6 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value=limit_buy_order_open), - get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, ) default_conf['max_open_trades'] = 0 @@ -523,7 +510,6 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_balance=MagicMock(return_value=20), get_fee=fee, ) default_conf['stake_amount'] = 10 From 7c8a367442b81c6d0913af47d91089bd88c3534f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 20:36:06 +0200 Subject: [PATCH 263/460] Update docs to not promote stoploss / take-profit --- docs/strategy-advanced.md | 23 ++++++++++++----------- freqtrade/optimize/backtesting.py | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f702b1448..3ef121091 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -44,9 +44,9 @@ class AwesomeStrategy(IStrategy): ## Custom sell signal -It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. +It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than 1 day: ``` python from freqtrade.strategy import IStrategy, timeframe_to_prev_date @@ -58,21 +58,22 @@ class AwesomeStrategy(IStrategy): trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() # Sell when price falls below value in stoploss column of taken buy signal. - if 'stop_loss' in trade_row: - if current_rate <= trade_row['stop_loss'] < trade.open_rate: - return 'stop_loss' + # above 20% profit, sell when rsi < 80 + if current_profit > 0.2: + if trade_row['rsi'] < 80: + return 'rsi_below_80' - # Sell when price reaches value in take_profit column of taken buy signal. - if 'take_profit' in trade_row: - if trade.open_rate < trade_row['take_profit'] <= current_rate: - return 'take_profit' + # Between 2% and 10%, sell if EMA-long above EMA-short + if 0.02 < current_profit < 0.1: + if trade_row['emalong'] > trade_row['emashort']: + return 'ema_long_below_80' # Sell any positions at a loss if they are held for more than two days. - if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: + if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. ## Custom stoploss diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b03a459ea..7b62661d3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -251,7 +251,8 @@ class Backtesting: sell_row: Tuple) -> Optional[LocalTrade]: sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore - sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], + sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], + sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: From 3285f6caa35ef555f8956eecd3468eae04c2e9c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 20:42:15 +0200 Subject: [PATCH 264/460] Improve wording in Note box --- docs/strategy-advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 3ef121091..58e658509 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -270,10 +270,10 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. !!! Note - `dataframe` is indexed by candle date. During dry/live runs `current_time` and - `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw - an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous - candle before using it as a `dataframe` index. + `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and + `trade.open_date_utc` will not match the candle date precisely and using them directly will throw + an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date + before using it to access `dataframe`. ``` python from freqtrade.exchange import timeframe_to_prev_date From 5bc908870f124f27fe16f890b895da8af970c2f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Apr 2021 09:07:47 +0200 Subject: [PATCH 265/460] Fix documentation comment missalignment --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 58e658509..49b310303 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,7 +46,7 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than 1 day: +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: ``` python from freqtrade.strategy import IStrategy, timeframe_to_prev_date @@ -68,7 +68,7 @@ class AwesomeStrategy(IStrategy): if trade_row['emalong'] > trade_row['emashort']: return 'ema_long_below_80' - # Sell any positions at a loss if they are held for more than two days. + # Sell any positions at a loss if they are held for more than one day. if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: return 'unclog' ``` From 7cf8c5d6592deddcf40dbaf00a56ec8159c94d17 Mon Sep 17 00:00:00 2001 From: Nial McCallister <48334675+nmcc1212@users.noreply.github.com> Date: Thu, 29 Apr 2021 10:46:00 +0100 Subject: [PATCH 266/460] Docker Quick start grammatical error please install docker-compose should be installed does not make grammatical sense --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 9096000c1..5f48782d2 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -10,7 +10,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -To simplify running freqtrade, please install [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). +To simplify running freqtrade, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). ## Freqtrade with docker-compose From 83708ae04596f7292a5a4e34a6a8ffd1c8908d33 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 29 Apr 2021 12:16:02 +0200 Subject: [PATCH 267/460] Update strategy-advanced.md Remove untrue comment probably left-over from more intricate example --- docs/strategy-advanced.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 49b310303..ccd4c2419 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -57,8 +57,7 @@ class AwesomeStrategy(IStrategy): trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() - # Sell when price falls below value in stoploss column of taken buy signal. - # above 20% profit, sell when rsi < 80 + # Above 20% profit, sell when rsi < 80 if current_profit > 0.2: if trade_row['rsi'] < 80: return 'rsi_below_80' From cf839e36f331983f74bc0fd1dd3bb6be73a61599 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 29 Apr 2021 12:49:51 +0200 Subject: [PATCH 268/460] Add to custom_sell() documentation - Flesh out infos about return type - give quick example at beginning to get reader in right mindset what this does and why it's useful --- docs/strategy-advanced.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 49b310303..68b3f201a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -44,7 +44,11 @@ class AwesomeStrategy(IStrategy): ## Custom sell signal -It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. +It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. + +For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. + +!!! Note Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set. `string` max length is 64 characters. Exceeding this limit will raise `OperationalException`. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: From f2bd70dfc24e74d56cbc8b42c1610fd8785bc41d Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 29 Apr 2021 13:07:22 +0200 Subject: [PATCH 269/460] Add sentence about how it differs from custom_stoploss() --- docs/strategy-advanced.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 68b3f201a..8023a18ab 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,7 +46,9 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. +For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. + +You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. !!! Note Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set. `string` max length is 64 characters. Exceeding this limit will raise `OperationalException`. From 4d1613a43265ea039b5b009335343920fe188063 Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Sat, 24 Apr 2021 13:26:40 +0200 Subject: [PATCH 270/460] Add chunks function. Implementing a generator to split Lists into chunks. --- freqtrade/misc.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 6508363d6..2e255901e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -6,7 +6,7 @@ import logging import re from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, Iterator, List from typing.io import IO import rapidjson @@ -202,3 +202,14 @@ def render_template_with_fallback(templatefile: str, templatefallbackfile: str, return render_template(templatefile, arguments) except TemplateNotFound: return render_template(templatefallbackfile, arguments) + + +def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]: + """ + Split lst into chunks of the size n. + :param lst: list to split into chunks + :param n: number of max elements per chunk + :return: None + """ + for chunk in range(0, len(lst), n): + yield (lst[chunk:chunk + n]) From 3be7bc509c603bc1f40619d61d12c34def88500c Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Sat, 24 Apr 2021 13:28:24 +0200 Subject: [PATCH 271/460] Telegram: send locks as chunks of 25. Producing easily readable messages, hopefully always below the message lenght limit --- freqtrade/rpc/telegram.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3eeedcd12..97a01fc53 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -20,7 +20,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN from freqtrade.exceptions import OperationalException -from freqtrade.misc import round_coin_value +from freqtrade.misc import chunks, round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -750,17 +750,18 @@ class Telegram(RPCHandler): Handler for /locks. Returns the currently active locks """ - locks = self._rpc._rpc_locks() - message = tabulate([[ - lock['id'], - lock['pair'], - lock['lock_end_time'], - lock['reason']] for lock in locks['locks']], - headers=['ID', 'Pair', 'Until', 'Reason'], - tablefmt='simple') - message = f"
{escape(message)}
" - logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + rpc_locks = self._rpc._rpc_locks() + for locks in chunks(rpc_locks['locks'], 25): + message = tabulate([[ + lock['id'], + lock['pair'], + lock['lock_end_time'], + lock['reason']] for lock in locks], + headers=['ID', 'Pair', 'Until', 'Reason'], + tablefmt='simple') + message = f"
{escape(message)}
" + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only def _delete_locks(self, update: Update, context: CallbackContext) -> None: From 6763bd447e040af00bfa9457a5916f70ee108f17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 07:50:33 +0200 Subject: [PATCH 272/460] Fix link to poweredby image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 916f9cf17..6fd59b62d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![freqtrade](docs/assets/freqtrade_poweredby.svg) +# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) From f3388ed9aa7ae903844213b0f5ca88fd43f7b91b Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Fri, 30 Apr 2021 14:30:37 +0200 Subject: [PATCH 273/460] fix IStrategy: abstract methods still need to pass through return value otherwise doing something like: ```py dataframe = super().populate_indicators(dataframe, ...) ``` won't work, because `dataframe` becomes `None`. This is needed if one of those methods uses dataframe.copy() instead of just working on reference. e.g. using `merge_informative` in `populate_indicator` in a nested class hierarchy --- freqtrade/strategy/interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 645e70e8a..63dcc75d9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -161,6 +161,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ + return dataframe @abstractmethod def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -170,6 +171,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ + return dataframe @abstractmethod def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -179,6 +181,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param metadata: Additional information, like the currently traded pair :return: DataFrame with sell column """ + return dataframe def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ From 856b65206bf61b14662f3761c0dba0df560db36d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 19:42:41 +0200 Subject: [PATCH 274/460] Reduce log-frequency of AgeFilter closes #4840 --- freqtrade/plugins/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 8a5379ca6..837e99f2a 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -71,7 +71,7 @@ class AgeFilter(IPairList): daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) - logger.info(f"Validated {len(pairlist)} pairs.") + self.log_once(f"Validated {len(pairlist)} pairs.", logger.info) return pairlist def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: From 42a52ff6694c4595e27df7f3c49accb7ae3448c8 Mon Sep 17 00:00:00 2001 From: Boris Pruessmann Date: Sat, 1 May 2021 14:13:21 +0200 Subject: [PATCH 275/460] Docker support for arm64 --- Dockerfile.aarch64 | 58 +++++++++++++++++++++++++++++++++ build_helpers/install_ta-lib.sh | 5 ++- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.aarch64 diff --git a/Dockerfile.aarch64 b/Dockerfile.aarch64 new file mode 100644 index 000000000..71c10d949 --- /dev/null +++ b/Dockerfile.aarch64 @@ -0,0 +1,58 @@ +FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base + +# Setup env +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONFAULTHANDLER 1 +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" + +# Prepare environment +RUN mkdir /freqtrade \ + && apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers + +WORKDIR /freqtrade + +# Install dependencies +FROM base as python-deps +RUN apt-get update \ + && apt-get -y install curl build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ + && apt-get clean \ + && pip install --upgrade pip + +# Install TA-lib +COPY build_helpers/* /tmp/ +RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* +ENV LD_LIBRARY_PATH /usr/local/lib + +# Install dependencies +COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ +USER ftuser +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-cache-dir -r requirements-hyperopt.txt + +# Copy dependencies to runtime-image +FROM base as runtime-image +COPY --from=python-deps /usr/local/lib /usr/local/lib +ENV LD_LIBRARY_PATH /usr/local/lib + +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local + +USER ftuser +# Install and execute +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + +ENTRYPOINT ["freqtrade"] +# Default to trade mode +CMD [ "trade" ] diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index cb86e5f64..dd87cf105 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -8,10 +8,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ + && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \ + && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ - && make \ + && make -j$(nproc) \ && which sudo && sudo make install || make install \ && cd .. else echo "TA-lib already installed, skipping installation" fi +# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ From e050ea8dfa671fd15cc6a124734ad1dfc9b3f82d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 15:33:03 +0200 Subject: [PATCH 276/460] Don't load parameters for other space --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32486136d..1848730dd 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -284,7 +284,7 @@ class HyperStrategyMixin(object): if not params: logger.info(f"No params for {space} found, using default values.") - for attr_name, attr in self.enumerate_parameters(): + for attr_name, attr in self.enumerate_parameters(space): attr.hyperopt = hyperopt if params and attr_name in params: if attr.load: From e381df9098faa6c3ef998c3aa96bf31ff8336e3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 16:36:35 +0200 Subject: [PATCH 277/460] extract has_space to Hyperopt-Tools --- freqtrade/optimize/hyperopt.py | 44 ++++++++++++---------------- freqtrade/optimize/hyperopt_tools.py | 13 +++++++- tests/optimize/test_hyperopt.py | 6 ++-- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d1dabff36..361480329 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -114,7 +114,7 @@ class Hyperopt: self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False) - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): # Make sure use_sell_signal is enabled if 'ask_strategy' not in self.config: self.config['ask_strategy'] = {} @@ -175,18 +175,18 @@ class Hyperopt: """ result: Dict = {} - if self.has_space('buy'): + if HyperoptTools.has_space(self.config, 'buy'): result['buy'] = {p.name: params.get(p.name) for p in self.hyperopt_space('buy')} - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): result['sell'] = {p.name: params.get(p.name) for p in self.hyperopt_space('sell')} - if self.has_space('roi'): + if HyperoptTools.has_space(self.config, 'roi'): result['roi'] = self.custom_hyperopt.generate_roi_table(params) - if self.has_space('stoploss'): + if HyperoptTools.has_space(self.config, 'stoploss'): result['stoploss'] = {p.name: params.get(p.name) for p in self.hyperopt_space('stoploss')} - if self.has_space('trailing'): + if HyperoptTools.has_space(self.config, 'trailing'): result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) return result @@ -208,16 +208,6 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - def has_space(self, space: str) -> bool: - """ - Tell if the space value is contained in the configuration - """ - # The 'trailing' space is not included in the 'default' set of spaces - if space == 'trailing': - return any(s in self.config['spaces'] for s in [space, 'all']) - else: - return any(s in self.config['spaces'] for s in [space, 'all', 'default']) - def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ Return the dimensions in the hyperoptimization space. @@ -227,23 +217,25 @@ class Hyperopt: """ spaces: List[Dimension] = [] - if space == 'buy' or (space is None and self.has_space('buy')): + if space == 'buy' or (space is None and HyperoptTools.has_space(self.config, 'buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() - if space == 'sell' or (space is None and self.has_space('sell')): + if space == 'sell' or (space is None and HyperoptTools.has_space(self.config, 'sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - if space == 'roi' or (space is None and self.has_space('roi')): + if space == 'roi' or (space is None and HyperoptTools.has_space(self.config, 'roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() - if space == 'stoploss' or (space is None and self.has_space('stoploss')): + if space == 'stoploss' or (space is None + and HyperoptTools.has_space(self.config, 'stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() - if space == 'trailing' or (space is None and self.has_space('trailing')): + if space == 'trailing' or (space is None + and HyperoptTools.has_space(self.config, 'trailing')): logger.debug("Hyperopt has 'trailing' space") spaces += self.custom_hyperopt.trailing_space() @@ -257,22 +249,22 @@ class Hyperopt: params_dict = self._get_params_dict(raw_params) params_details = self._get_params_details(params_dict) - if self.has_space('roi'): + if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) - if self.has_space('buy'): + if HyperoptTools.has_space(self.config, 'buy'): self.backtesting.strategy.advise_buy = ( # type: ignore self.custom_hyperopt.buy_strategy_generator(params_dict)) - if self.has_space('sell'): + if HyperoptTools.has_space(self.config, 'sell'): self.backtesting.strategy.advise_sell = ( # type: ignore self.custom_hyperopt.sell_strategy_generator(params_dict)) - if self.has_space('stoploss'): + if HyperoptTools.has_space(self.config, 'stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] - if self.has_space('trailing'): + if HyperoptTools.has_space(self.config, 'trailing'): d = self.custom_hyperopt.generate_trailing_params(params_dict) self.backtesting.strategy.trailing_stop = d['trailing_stop'] self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive'] diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index d4c347f80..c39e0b943 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -4,7 +4,7 @@ import logging from collections import OrderedDict from pathlib import Path from pprint import pformat -from typing import Dict, List +from typing import Any, Dict, List import rapidjson import tabulate @@ -21,6 +21,17 @@ logger = logging.getLogger(__name__) class HyperoptTools(): + @staticmethod + def has_space(config: Dict[str, Any], space: str) -> bool: + """ + Tell if the space value is contained in the configuration + """ + # The 'trailing' space is not included in the 'default' set of spaces + if space == 'trailing': + return any(s in config['spaces'] for s in [space, 'all']) + else: + return any(s in config['spaces'] for s in [space, 'all', 'default']) + @staticmethod def _read_results(results_file: Path) -> List: """ diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 90ff8a1d0..d125cfca3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -503,10 +503,10 @@ def test_format_results(hyperopt): (['default', 'buy'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), ]) -def test_has_space(hyperopt, spaces, expected_results): +def test_has_space(hyperopt_conf, spaces, expected_results): for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - hyperopt.config.update({'spaces': spaces}) - assert hyperopt.has_space(s) == expected_results[s] + hyperopt_conf.update({'spaces': spaces}) + assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s] def test_populate_indicators(hyperopt, testdatadir) -> None: From 555262b6e1f2c28ff03b27dc03452efb17029044 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 16:36:53 +0200 Subject: [PATCH 278/460] Only calculate additional indicators if the space is selected --- freqtrade/strategy/hyper.py | 9 ++++++--- tests/optimize/test_hyperopt.py | 2 +- tests/strategy/test_interface.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 1848730dd..2714ffb43 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -7,6 +7,8 @@ from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union +from freqtrade.optimize.hyperopt_tools import HyperoptTools + with suppress(ImportError): from skopt.space import Integer, Real, Categorical @@ -26,7 +28,7 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - hyperopt: bool = False + in_space: bool = False def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -131,7 +133,7 @@ class IntParameter(NumericParameter): Returns a List with 1 item (`value`) in "non-hyperopt" mode, to avoid calculating 100ds of indicators. """ - if self.hyperopt: + if self.in_space and self.optimize: # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive return range(self.low, self.high + 1) else: @@ -247,6 +249,7 @@ class HyperStrategyMixin(object): """ Initialize hyperoptable strategy mixin. """ + self.config = config self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: @@ -285,7 +288,7 @@ class HyperStrategyMixin(object): logger.info(f"No params for {space} found, using default values.") for attr_name, attr in self.enumerate_parameters(space): - attr.hyperopt = hyperopt + attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) if params and attr_name in params: if attr.load: attr.value = params[attr_name] diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d125cfca3..5a7f0f32e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1108,7 +1108,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.buy_rsi.hyperopt is True + assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index bd81bc80c..4ca514349 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -636,7 +636,7 @@ def test_hyperopt_parameters(): assert len(list(intpar.range)) == 1 # Range contains ONLY the default / value. assert list(intpar.range) == [intpar.value] - intpar.hyperopt = True + intpar.in_space = True assert len(list(intpar.range)) == 6 assert list(intpar.range) == [0, 1, 2, 3, 4, 5] From ca0749dfdd6d2b2291bc366f813e070f33a3560e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 16:58:14 +0200 Subject: [PATCH 279/460] Update strategy-advanced.md --- docs/strategy-advanced.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 8023a18ab..0580b2d39 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -50,7 +50,8 @@ For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. -!!! Note Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set. `string` max length is 64 characters. Exceeding this limit will raise `OperationalException`. +!!! Note + Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: From 0b280a59bc4413e46e4277b2a66e95d3baca3378 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:29:53 +0200 Subject: [PATCH 280/460] Support per exchange params for OHLCV endpoint --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/exchange.py | 15 +++++---------- freqtrade/exchange/hitbtc.py | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 freqtrade/exchange/hitbtc.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 889bb49c2..23ba2eb10 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -14,5 +14,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, timeframe_to_seconds, validate_exchange, validate_exchanges) from freqtrade.exchange.ftx import Ftx +from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 809cdb4e1..6642b946d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -59,6 +59,7 @@ class Exchange: _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], + "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_partial_candle": True, "trades_pagination": "time", # Possible are "time" or "id" @@ -874,17 +875,11 @@ class Exchange: "Fetching pair %s, interval %s, since %s %s...", pair, timeframe, since_ms, s ) - #fixing support for HitBTC #4778 - if self.name== 'HitBTC': - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, - since=since_ms, - limit=self.ohlcv_candle_limit(timeframe), - params={"sort": "DESC"} - ) - else: - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, + params = self._ft_has.get('ohlcv_params', {}) + data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms, - limit=self.ohlcv_candle_limit(timeframe)) + limit=self.ohlcv_candle_limit(timeframe), + params=params) # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py new file mode 100644 index 000000000..b9d32b56d --- /dev/null +++ b/freqtrade/exchange/hitbtc.py @@ -0,0 +1,26 @@ +""" hitbtx exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Hitbtc(Exchange): + """ + Hitbtc exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + # fetchCurrencies API point requires authentication for Hitbtc, + _ft_has: Dict = { + "ohlcv_candle_limit": 1000, + + "ohlcv_params": {"sort": "DESC"} + } From bdd0184f0bdd9329d9aaada8f7fbf91146fe0d41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 17:44:43 +0200 Subject: [PATCH 281/460] Small stylistic fixes --- freqtrade/exchange/hitbtc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index b9d32b56d..763535263 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -1,4 +1,3 @@ -""" hitbtx exchange subclass """ import logging from typing import Dict @@ -21,6 +20,5 @@ class Hitbtc(Exchange): # fetchCurrencies API point requires authentication for Hitbtc, _ft_has: Dict = { "ohlcv_candle_limit": 1000, - "ohlcv_params": {"sort": "DESC"} } From ac2e1eb3d750ac79dfb692f7fa1c6fce4fda5536 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 08:44:16 +0200 Subject: [PATCH 282/460] Don't import joblib for regular strategies --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index c39e0b943..a223a68bb 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -9,7 +9,6 @@ from typing import Any, Dict, List import rapidjson import tabulate from colorama import Fore, Style -from joblib import load from pandas import isna, json_normalize from freqtrade.exceptions import OperationalException @@ -37,6 +36,8 @@ class HyperoptTools(): """ Read hyperopt results from file """ + from joblib import load + logger.info("Reading epochs from '%s'", results_file) data = load(results_file) return data From c45204a2c4ef106bde1a37288a6d2f054de9864f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 08:59:21 +0200 Subject: [PATCH 283/460] Fix failing mocks --- tests/optimize/test_hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 5a7f0f32e..cd405c52f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -340,7 +340,7 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + mock_load = mocker.patch('joblib.load', return_value=epochs) results_file = testdatadir / 'optimize' / 'ut_results.pickle' hyperopt_epochs = HyperoptTools._read_results(results_file) assert log_has(f"Reading epochs from '{results_file}'", caplog) @@ -350,7 +350,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + mock_load = mocker.patch('joblib.load', return_value=epochs) mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) statmock = MagicMock() statmock.st_size = 5 @@ -364,7 +364,7 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: mock_load.assert_called_once() del epochs[0]['is_best'] - mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) + mock_load = mocker.patch('joblib.load', return_value=epochs) with pytest.raises(OperationalException): HyperoptTools.load_previous_results(results_file) From b125c975c746a6de9f16883906b2892eba7d2dd2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 15:34:15 +0200 Subject: [PATCH 284/460] Rename strategy_comparison method --- freqtrade/optimize/optimize_reports.py | 4 ++-- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a80dc5d31..91ca619be 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -153,7 +153,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List return tabular_data -def generate_strategy_metrics(all_results: Dict) -> List[Dict]: +def generate_strategy_comparison(all_results: Dict) -> List[Dict]: """ Generate summary per strategy :param all_results: Dict of containing results for all strategies @@ -370,7 +370,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'csum_max': 0 }) - strategy_results = generate_strategy_metrics(all_results=all_results) + strategy_results = generate_strategy_comparison(all_results=all_results) result['strategy_comparison'] = strategy_results diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 00114be5b..39625978b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -817,7 +817,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): text_table_strategy=strattable_mock, generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, - generate_strategy_metrics=strat_summary, + generate_strategy_comparison=strat_summary, generate_daily_stats=MagicMock(), ) patched_configuration_load_config_file(mocker, default_conf) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8119c732b..83a555eab 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -14,7 +14,7 @@ from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_metrics, store_backtest_stats, + generate_strategy_comparison, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -345,7 +345,7 @@ def test_text_table_strategy(default_conf): ' 43.33 | 0:20:00 | 3 | 0 | 0 |' ) - strategy_results = generate_strategy_metrics(all_results=results) + strategy_results = generate_strategy_comparison(all_results=results) assert text_table_strategy(strategy_results, 'BTC') == result_str From 9994fce5778efe937866a8fdc26dc95abe8fdfa7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 15:44:21 +0200 Subject: [PATCH 285/460] Extract generation of report for one strategy to it's own method --- freqtrade/optimize/optimize_reports.py | 257 +++++++++++++------------ 1 file changed, 139 insertions(+), 118 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 91ca619be..682483adb 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -235,6 +235,142 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: } +def generate_strategy_stats(btdata: Dict[str, DataFrame], + strategy: str, + content: Dict[str, Any], + min_date: Arrow, max_date: Arrow, + market_change: float + ) -> Dict[str, Any]: + """ + :param btdata: Backtest data + :param strategy: Strategy name + :param content: Backtest result data in the format: + {'results: results, 'config: config}}. + :param min_date: Backtest start date + :param max_date: Backtest end date + :param market_change: float indicating the market change + :return: Dictionary containing results per strategy and a stratgy summary. + """ + results: Dict[str, DataFrame] = content['results'] + if not isinstance(results, DataFrame): + return + config = content['config'] + max_open_trades = min(config['max_open_trades'], len(btdata.keys())) + starting_balance = config['dry_run_wallet'] + stake_currency = config['stake_currency'] + + pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results, skip_nan=False) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, + results=results) + left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results.loc[results['is_open']], + skip_nan=True) + daily_stats = generate_daily_stats(results) + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 + results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 + + backtest_days = (max_date - min_date).days + strat_stats = { + 'trades': results.to_dict(orient='records'), + 'locks': [lock.to_json() for lock in content['locks']], + 'best_pair': best_pair, + 'worst_pair': worst_pair, + 'results_per_pair': pair_results, + 'sell_reason_summary': sell_reason_stats, + 'left_open_trades': left_open_results, + 'total_trades': len(results), + 'total_volume': float(results['stake_amount'].sum()), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, + 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_total': results['profit_abs'].sum() / starting_balance, + 'profit_total_abs': results['profit_abs'].sum(), + 'backtest_start': min_date.datetime, + 'backtest_start_ts': min_date.int_timestamp * 1000, + 'backtest_end': max_date.datetime, + 'backtest_end_ts': max_date.int_timestamp * 1000, + 'backtest_days': backtest_days, + + 'backtest_run_start_ts': content['backtest_start_time'], + 'backtest_run_end_ts': content['backtest_end_time'], + + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, + 'market_change': market_change, + 'pairlist': list(btdata.keys()), + 'stake_amount': config['stake_amount'], + 'stake_currency': config['stake_currency'], + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), + 'starting_balance': starting_balance, + 'dry_run_wallet': starting_balance, + 'final_balance': content['final_balance'], + 'max_open_trades': max_open_trades, + 'max_open_trades_setting': (config['max_open_trades'] + if config['max_open_trades'] != float('inf') else -1), + 'timeframe': config['timeframe'], + 'timerange': config.get('timerange', ''), + 'enable_protections': config.get('enable_protections', False), + 'strategy_name': strategy, + # Parameters relevant for backtesting + 'stoploss': config['stoploss'], + 'trailing_stop': config.get('trailing_stop', False), + 'trailing_stop_positive': config.get('trailing_stop_positive'), + 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), + 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), + 'use_custom_stoploss': config.get('use_custom_stoploss', False), + 'minimal_roi': config['minimal_roi'], + 'use_sell_signal': config['ask_strategy']['use_sell_signal'], + 'sell_profit_only': config['ask_strategy']['sell_profit_only'], + 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], + 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], + **daily_stats, + } + + try: + max_drawdown, _, _, _, _ = calculate_max_drawdown( + results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') + strat_stats.update({ + 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, + 'drawdown_start': drawdown_start, + 'drawdown_start_ts': drawdown_start.timestamp() * 1000, + 'drawdown_end': drawdown_end, + 'drawdown_end_ts': drawdown_end.timestamp() * 1000, + + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, + }) + + csum_min, csum_max = calculate_csum(results, starting_balance) + strat_stats.update({ + 'csum_min': csum_min, + 'csum_max': csum_max + }) + + except ValueError: + strat_stats.update({ + 'max_drawdown': 0.0, + 'max_drawdown_abs': 0.0, + 'max_drawdown_low': 0.0, + 'max_drawdown_high': 0.0, + 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_start_ts': 0, + 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_end_ts': 0, + 'csum_min': 0, + 'csum_max': 0 + }) + + return strat_stats + + def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], min_date: Arrow, max_date: Arrow @@ -245,131 +381,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date - :return: - Dictionary containing results per strategy and a stratgy summary. + :return: Dictionary containing results per strategy and a stratgy summary. """ result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, content in all_results.items(): - results: Dict[str, DataFrame] = content['results'] - if not isinstance(results, DataFrame): - continue - config = content['config'] - max_open_trades = min(config['max_open_trades'], len(btdata.keys())) - starting_balance = config['dry_run_wallet'] - stake_currency = config['stake_currency'] - - pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, - results=results) - left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results.loc[results['is_open']], - skip_nan=True) - daily_stats = generate_daily_stats(results) - best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], - key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], - key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 - results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 - - backtest_days = (max_date - min_date).days - strat_stats = { - 'trades': results.to_dict(orient='records'), - 'locks': [lock.to_json() for lock in content['locks']], - 'best_pair': best_pair, - 'worst_pair': worst_pair, - 'results_per_pair': pair_results, - 'sell_reason_summary': sell_reason_stats, - 'left_open_trades': left_open_results, - 'total_trades': len(results), - 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, - 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_abs'].sum() / starting_balance, - 'profit_total_abs': results['profit_abs'].sum(), - 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.int_timestamp * 1000, - 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.int_timestamp * 1000, - 'backtest_days': backtest_days, - - 'backtest_run_start_ts': content['backtest_start_time'], - 'backtest_run_end_ts': content['backtest_end_time'], - - 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, - 'market_change': market_change, - 'pairlist': list(btdata.keys()), - 'stake_amount': config['stake_amount'], - 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), - 'starting_balance': starting_balance, - 'dry_run_wallet': starting_balance, - 'final_balance': content['final_balance'], - 'max_open_trades': max_open_trades, - 'max_open_trades_setting': (config['max_open_trades'] - if config['max_open_trades'] != float('inf') else -1), - 'timeframe': config['timeframe'], - 'timerange': config.get('timerange', ''), - 'enable_protections': config.get('enable_protections', False), - 'strategy_name': strategy, - # Parameters relevant for backtesting - 'stoploss': config['stoploss'], - 'trailing_stop': config.get('trailing_stop', False), - 'trailing_stop_positive': config.get('trailing_stop_positive'), - 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), - 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), - 'use_custom_stoploss': config.get('use_custom_stoploss', False), - 'minimal_roi': config['minimal_roi'], - 'use_sell_signal': config['ask_strategy']['use_sell_signal'], - 'sell_profit_only': config['ask_strategy']['sell_profit_only'], - 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], - 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], - **daily_stats, - } + strat_stats = generate_strategy_stats(btdata, strategy, content, + min_date, max_date, market_change=market_change) result['strategy'][strategy] = strat_stats - try: - max_drawdown, _, _, _, _ = calculate_max_drawdown( - results, value_col='profit_ratio') - drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( - results, value_col='profit_abs') - strat_stats.update({ - 'max_drawdown': max_drawdown, - 'max_drawdown_abs': drawdown_abs, - 'drawdown_start': drawdown_start, - 'drawdown_start_ts': drawdown_start.timestamp() * 1000, - 'drawdown_end': drawdown_end, - 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, - }) - - csum_min, csum_max = calculate_csum(results, starting_balance) - strat_stats.update({ - 'csum_min': csum_min, - 'csum_max': csum_max - }) - - except ValueError: - strat_stats.update({ - 'max_drawdown': 0.0, - 'max_drawdown_abs': 0.0, - 'max_drawdown_low': 0.0, - 'max_drawdown_high': 0.0, - 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_start_ts': 0, - 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_end_ts': 0, - 'csum_min': 0, - 'csum_max': 0 - }) - strategy_results = generate_strategy_comparison(all_results=all_results) result['strategy_comparison'] = strategy_results From 545cba7fd833d7e9b400e34661ae44e60e9437f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Apr 2021 19:28:32 +0200 Subject: [PATCH 286/460] Refactor optimize_report we should not calculate non-daily statistics in the daily stats method --- freqtrade/optimize/optimize_reports.py | 42 +++++++++++++++++++------ tests/optimize/test_optimize_reports.py | 21 +++++++++++-- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 682483adb..77259df87 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -194,7 +194,37 @@ def generate_edge_table(results: dict) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore +def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: + """ Generate overall trade statistics """ + if len(results) == 0: + return { + 'wins': 0, + 'losses': 0, + 'draws': 0, + 'holding_avg': timedelta(), + 'winner_holding_avg': timedelta(), + 'loser_holding_avg': timedelta(), + } + + winning_trades = results.loc[results['profit_ratio'] > 0] + draw_trades = results.loc[results['profit_ratio'] == 0] + losing_trades = results.loc[results['profit_ratio'] < 0] + + return { + 'wins': len(winning_trades), + 'losses': len(losing_trades), + 'draws': len(draw_trades), + 'holding_avg': (timedelta(minutes=round(results['trade_duration'].mean())) + if not results.empty else timedelta()), + 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) + if not winning_trades.empty else timedelta()), + 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) + if not losing_trades.empty else timedelta()), + } + + def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: + """ Generate daily statistics """ if len(results) == 0: return { 'backtest_best_day': 0, @@ -204,8 +234,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': 0, 'draw_days': 0, 'losing_days': 0, - 'winner_holding_avg': timedelta(), - 'loser_holding_avg': timedelta(), } daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) @@ -217,9 +245,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: draw_days = sum(daily_profit == 0) losing_days = sum(daily_profit < 0) - winning_trades = results.loc[results['profit_ratio'] > 0] - losing_trades = results.loc[results['profit_ratio'] < 0] - return { 'backtest_best_day': best_rel, 'backtest_worst_day': worst_rel, @@ -228,10 +253,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, - 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) - if not winning_trades.empty else timedelta()), - 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) - if not losing_trades.empty else timedelta()), } @@ -269,6 +290,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) + trade_stats = generate_trading_stats(results) best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], @@ -289,6 +311,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, @@ -329,6 +352,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, + **trade_stats } try: diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 83a555eab..b268ebadc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -14,7 +14,7 @@ from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_comparison, store_backtest_stats, + generate_strategy_comparison, generate_trading_stats, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -226,8 +226,6 @@ def test_generate_daily_stats(testdatadir): assert res['winning_days'] == 14 assert res['draw_days'] == 4 assert res['losing_days'] == 3 - assert res['winner_holding_avg'] == timedelta(seconds=1440) - assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) # Select empty dataframe! res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) @@ -238,6 +236,23 @@ def test_generate_daily_stats(testdatadir): assert res['losing_days'] == 0 +def test_generate_trading_stats(testdatadir): + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_data(filename) + res = generate_trading_stats(bt_data) + assert isinstance(res, dict) + assert res['winner_holding_avg'] == timedelta(seconds=1440) + assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) + assert 'wins' in res + assert 'losses' in res + assert 'draws' in res + + # Select empty dataframe! + res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) + assert res['wins'] == 0 + assert res['losses'] == 0 + + def test_text_table_sell_reason(): results = pd.DataFrame( From 6aaaad29d7edfb095ca83ab7ad3342663f74e7cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 22:33:58 +0200 Subject: [PATCH 287/460] Use backtesting output for hyperopt results --- freqtrade/optimize/hyperopt.py | 68 ++++++++++++++-------------- freqtrade/optimize/hyperopt_tools.py | 41 +++++++++++------ 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 361480329..468dfd873 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -8,7 +8,7 @@ import locale import logging import random import warnings -from datetime import datetime +from datetime import datetime, timezone from math import ceil from operator import itemgetter from pathlib import Path @@ -30,6 +30,8 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats +from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -79,9 +81,8 @@ class Hyperopt: self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") strategy = str(self.config['strategy']) - self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / - f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') + self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' / + f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) @@ -246,6 +247,7 @@ class Hyperopt: Used Optimize function. Called once per epoch to optimize whatever is configured. Keep this function as optimized as possible! """ + backtest_start_time = datetime.now(timezone.utc) params_dict = self._get_params_dict(raw_params) params_details = self._get_params_details(params_dict) @@ -284,19 +286,31 @@ class Hyperopt: 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, + backtest_end_time = datetime.now(timezone.utc) + + bt_result = { + 'results': backtesting_results, + 'config': self.backtesting.strategy.config, + 'locks': PairLocks.get_all_locks(), + 'final_balance': self.backtesting.wallets.get_total( + self.backtesting.strategy.config['stake_currency']), + 'backtest_start_time': int(backtest_start_time.timestamp()), + 'backtest_end_time': int(backtest_end_time.timestamp()), + } + return self._get_results_dict(bt_result, min_date, max_date, params_dict, params_details, processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, params_dict, params_details, processed: Dict[str, DataFrame]): - results_metrics = self._calculate_results_metrics(backtesting_results) - results_explanation = self._format_results_explanation_string(results_metrics) - trade_count = results_metrics['trade_count'] - total_profit = results_metrics['total_profit'] + strat_stats = generate_strategy_stats(processed, '', backtesting_results, + min_date, max_date, market_change=0) + results_explanation = self._format_results_explanation_string(strat_stats) + + trade_count = strat_stats['total_trades'] + total_profit = strat_stats['profit_total'] # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -304,48 +318,32 @@ class Hyperopt: # path. We do not want to optimize 'hodl' strategies. loss: float = MAX_LOSS if trade_count >= self.config['hyperopt_min_trades']: - loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, + loss = self.calculate_loss(results=backtesting_results['results'], + trade_count=trade_count, min_date=min_date.datetime, max_date=max_date.datetime, config=self.config, processed=processed) return { 'loss': loss, 'params_dict': params_dict, 'params_details': params_details, - 'results_metrics': results_metrics, + 'results_metrics': strat_stats, 'results_explanation': results_explanation, 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: - wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0]) - draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0]) - losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0]) - return { - 'trade_count': len(backtesting_results.index), - 'wins': wins, - 'draws': draws, - 'losses': losses, - 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", - 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0, - 'median_profit': backtesting_results['profit_ratio'].median() * 100.0, - 'total_profit': backtesting_results['profit_abs'].sum(), - 'profit': backtesting_results['profit_ratio'].sum() * 100.0, - 'duration': backtesting_results['trade_duration'].mean(), - } - def _format_results_explanation_string(self, results_metrics: Dict) -> str: """ Return the formatted results explanation in a string """ stake_cur = self.config['stake_currency'] - return (f"{results_metrics['trade_count']:6d} trades. " + return (f"{results_metrics['total_trades']:6d} trades. " f"{results_metrics['wins']}/{results_metrics['draws']}" f"/{results_metrics['losses']} Wins/Draws/Losses. " - f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " - f"Median profit {results_metrics['median_profit']: 6.2f}%. " - f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " - f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['duration']:5.1f} min." + f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " + f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_cur} " + f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"Avg duration {results_metrics['holding_avg']} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index a223a68bb..1635c0588 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -12,7 +12,7 @@ from colorama import Fore, Style from pandas import isna, json_normalize from freqtrade.exceptions import OperationalException -from freqtrade.misc import round_dict +from freqtrade.misc import round_coin_value, round_dict logger = logging.getLogger(__name__) @@ -169,11 +169,24 @@ class HyperoptTools(): # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] + if 'results_metrics.total_trades' in trials: + # New mode, using backtest result for metrics + trials['results_metrics.winsdrawslosses'] = trials.apply( + lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " + f"{x['results_metrics.losses']:>4}", axis=1) + trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.winsdrawslosses', + 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', + 'results_metrics.profit_total', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best']] + else: + # Legacy mode + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', 'Total profit', 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] @@ -188,21 +201,23 @@ class HyperoptTools(): lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') ) trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" + if not isna(x) else "--".rjust(7, ' ') ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') ) + stake_currency = config['stake_currency'] trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], + lambda x: '{} {}'.format( + round_coin_value(x['Total profit'], stake_currency), '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + ).rjust(25+len(stake_currency)) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), axis=1 ) trials = trials.drop(columns=['Total profit']) From 852f12534746e886f46227f1c610efe9786b5d0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Apr 2021 20:17:13 +0200 Subject: [PATCH 288/460] Fix tests --- tests/optimize/test_hyperopt.py | 128 +++++++++++++----------- tests/optimize/test_optimize_reports.py | 3 +- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index cd405c52f..1b1aaebad 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -5,7 +5,7 @@ import re from datetime import datetime from pathlib import Path from typing import Dict, List -from unittest.mock import MagicMock +from unittest.mock import ANY, MagicMock import pandas as pd import pytest @@ -18,10 +18,12 @@ from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.hyper import IntParameter +from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -433,18 +435,41 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: assert hasattr(hyperopt, "position_stacking") -def test_format_results(hyperopt): - # Test with BTC as stake_currency - trades = [ - ('ETH/BTC', 2, 2, 123), - ('LTC/BTC', 1, 1, 123), - ('XPR/BTC', -1, -2, -246) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - df = pd.DataFrame.from_records(trades, columns=labels) - results_metrics = hyperopt._calculate_results_metrics(df) +def test_hyperopt_format_results(hyperopt): + + bt_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt.config, + 'locks': [], + 'final_balance': 0.02, + 'backtest_start_time': 1619718665, + 'backtest_end_time': 1619718665, + } + results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, + Arrow(2017, 11, 14, 19, 32, 00), + Arrow(2017, 12, 14, 19, 32, 00), market_change=0) + results_explanation = hyperopt._format_results_explanation_string(results_metrics) - total_profit = results_metrics['total_profit'] + total_profit = results_metrics['profit_total_abs'] results = { 'loss': 0.0, @@ -458,21 +483,9 @@ def test_format_results(hyperopt): } result = HyperoptTools._format_explanation_string(results, 1) - assert result.find(' 66.67%') - assert result.find('Total profit 1.00000000 BTC') - assert result.find('2.0000Σ %') - - # Test with EUR as stake_currency - trades = [ - ('ETH/EUR', 2, 2, 123), - ('LTC/EUR', 1, 1, 123), - ('XPR/EUR', -1, -2, -246) - ] - df = pd.DataFrame.from_records(trades, columns=labels) - results_metrics = hyperopt._calculate_results_metrics(df) - results['total_profit'] = results_metrics['total_profit'] - result = HyperoptTools._format_explanation_string(results, 1) - assert result.find('Total profit 1.00000000 EUR') + assert ' 0.71%' in result + assert 'Total profit 0.00003100 BTC' in result + assert '0:50:00 min' in result @pytest.mark.parametrize("spaces, expected_results", [ @@ -577,22 +590,32 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'hyperopt_min_trades': 1, }) - trades = [ - ('TRX/BTC', 0.023117, 0.000233, 100) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - backtest_result = pd.DataFrame.from_records(trades, columns=labels) + backtest_result = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }) - mocker.patch( - 'freqtrade.optimize.hyperopt.Backtesting.backtest', - MagicMock(return_value=backtest_result) - ) - mocker.patch( - 'freqtrade.optimize.hyperopt.get_timerange', - MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) - ) + mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) + mocker.patch('freqtrade.optimize.hyperopt.get_timerange', + return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) - mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { 'adx-value': 0, @@ -626,11 +649,11 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_only_offset_is_reached': False, } response_expected = { - 'loss': 1.9840569076926293, - 'results_explanation': (' 1 trades. 1/0/0 Wins/Draws/Losses. ' - 'Avg profit 2.31%. Median profit 2.31%. Total profit ' - '0.00023300 BTC ( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). ' - 'Avg duration 100.0 min.' + 'loss': 1.9147239021396234, + 'results_explanation': (' 4 trades. 4/0/0 Wins/Draws/Losses. ' + 'Avg profit 0.77%. Median profit 0.71%. Total profit ' + '0.00003100 BTC ( 0.00\N{GREEK CAPITAL LETTER SIGMA}%). ' + 'Avg duration 0:50:00 min.' ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, @@ -660,17 +683,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, - 'results_metrics': {'avg_profit': 2.3117, - 'draws': 0, - 'duration': 100.0, - 'losses': 0, - 'winsdrawslosses': ' 1 0 0', - 'median_profit': 2.3117, - 'profit': 2.3117, - 'total_profit': 0.000233, - 'trade_count': 1, - 'wins': 1}, - 'total_profit': 0.00023300 + 'results_metrics': ANY, + 'total_profit': 3.1e-08 } hyperopt = Hyperopt(hyperopt_conf) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b268ebadc..fb4624ca3 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -14,7 +14,8 @@ from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_comparison, generate_trading_stats, store_backtest_stats, + generate_strategy_comparison, + generate_trading_stats, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver From e2e1d34828faa71936c61cf729535e2071918fdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Apr 2021 20:20:26 +0200 Subject: [PATCH 289/460] Extract stake_currency param from hyperopt-explanationstring --- freqtrade/optimize/hyperopt.py | 8 ++++---- tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 468dfd873..9b3a22236 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -307,7 +307,8 @@ class Hyperopt: strat_stats = generate_strategy_stats(processed, '', backtesting_results, min_date, max_date, market_change=0) - results_explanation = self._format_results_explanation_string(strat_stats) + results_explanation = self._format_results_explanation_string( + strat_stats, self.config['stake_currency']) trade_count = strat_stats['total_trades'] total_profit = strat_stats['profit_total'] @@ -331,17 +332,16 @@ class Hyperopt: 'total_profit': total_profit, } - def _format_results_explanation_string(self, results_metrics: Dict) -> str: + def _format_results_explanation_string(self, results_metrics: Dict, stake_currency: str) -> str: """ Return the formatted results explanation in a string """ - stake_cur = self.config['stake_currency'] return (f"{results_metrics['total_trades']:6d} trades. " f"{results_metrics['wins']}/{results_metrics['draws']}" f"/{results_metrics['losses']} Wins/Draws/Losses. " f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " - f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_cur} " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['holding_avg']} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1b1aaebad..a80f3dced 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -468,7 +468,7 @@ def test_hyperopt_format_results(hyperopt): Arrow(2017, 11, 14, 19, 32, 00), Arrow(2017, 12, 14, 19, 32, 00), market_change=0) - results_explanation = hyperopt._format_results_explanation_string(results_metrics) + results_explanation = hyperopt._format_results_explanation_string(results_metrics, 'BTC') total_profit = results_metrics['profit_total_abs'] results = { From f2e182002d9990f344faf537f30bee9ec9078914 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 07:31:57 +0200 Subject: [PATCH 290/460] Simplify calling backtesting by returning the proper result --- freqtrade/optimize/backtesting.py | 20 +++-- freqtrade/optimize/hyperopt.py | 16 ++-- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_backtesting.py | 111 +++++++++++++++---------- 4 files changed, 83 insertions(+), 66 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7b62661d3..54e7b806f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -330,7 +330,7 @@ class Backtesting: def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, - enable_protections: bool = False) -> DataFrame: + enable_protections: bool = False) -> Dict[str, Any]: """ Implement backtesting functionality @@ -417,7 +417,13 @@ class Backtesting: trades += self.handle_left_open(open_trades, data=data) self.wallets.update() - return trade_list_to_dataframe(trades) + results = trade_list_to_dataframe(trades) + return { + 'results': results, + 'config': self.strategy.config, + 'locks': PairLocks.get_all_locks(), + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + } def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) @@ -457,14 +463,12 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - self.all_results[self.strategy.get_strategy_name()] = { - 'results': results, - 'config': self.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), + results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } + }) + self.all_results[self.strategy.get_strategy_name()] = results + return min_date, max_date def start(self) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9b3a22236..229b35dd5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -31,7 +31,6 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats -from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -279,7 +278,7 @@ class Hyperopt: min_date, max_date = get_timerange(processed) - backtesting_results = self.backtesting.backtest( + bt_results = self.backtesting.backtest( processed=processed, start_date=min_date.datetime, end_date=max_date.datetime, @@ -288,17 +287,12 @@ class Hyperopt: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - - bt_result = { - 'results': backtesting_results, - 'config': self.backtesting.strategy.config, - 'locks': PairLocks.get_all_locks(), - 'final_balance': self.backtesting.wallets.get_total( - self.backtesting.strategy.config['stake_currency']), + bt_results.update({ 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), - } - return self._get_results_dict(bt_result, min_date, max_date, + }) + + return self._get_results_dict(bt_results, min_date, max_date, params_dict, params_details, processed=processed) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 77259df87..ef5426c9a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -274,7 +274,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], """ results: Dict[str, DataFrame] = content['results'] if not isinstance(results, DataFrame): - return + return {} config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) starting_balance = config['dry_run_wallet'] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 39625978b..e09f02c66 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -514,13 +514,14 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: timerange=timerange) processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) - results = backtesting.backtest( + result = backtesting.backtest( processed=processed, start_date=min_date, end_date=max_date, max_open_trades=10, position_stacking=False, ) + results = result['results'] assert not results.empty assert len(results) == 2 @@ -583,8 +584,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None max_open_trades=1, position_stacking=False, ) - assert not results.empty - assert len(results) == 1 + assert not results['results'].empty + assert len(results['results']) == 1 def test_processed(default_conf, mocker, testdatadir) -> None: @@ -623,7 +624,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres @pytest.mark.parametrize('protections,contour,expected', [ @@ -648,7 +649,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results - assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == expected + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): @@ -662,8 +663,8 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_only_sell(mocker, default_conf, testdatadir): @@ -677,8 +678,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(**backtest_conf) - assert results.empty + result = backtesting.backtest(**backtest_conf) + assert result['results'].empty def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): @@ -690,10 +691,11 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override - results = backtesting.backtest(**backtest_conf) + result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) # 100 buys signals + results = result['results'] assert len(results) == 100 # One trade was force-closed at the end assert len(results.loc[results['is_open']]) == 0 @@ -745,9 +747,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) results = backtesting.backtest(**backtest_conf) # Make sure we have parallel trades - assert len(evaluate_result_multi(results, '5m', 2)) > 0 + assert len(evaluate_result_multi(results['results'], '5m', 2)) > 0 # make sure we don't have trades with more than configured max_open_trades - assert len(evaluate_result_multi(results, '5m', 3)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 backtest_conf = { 'processed': processed, @@ -757,7 +759,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) 'position_stacking': False, } results = backtesting.backtest(**backtest_conf) - assert len(evaluate_result_multi(results, '5m', 1)) == 0 + assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0 def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @@ -803,7 +805,12 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS)) + backtestmock = MagicMock(return_value={ + 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) @@ -867,39 +874,51 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): patch_exchange(mocker) + result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'trade_duration': [235, 40], + 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_ratio': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'trade_duration': [47, 40, 20], + 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }) backtestmock = MagicMock(side_effect=[ - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], - 'profit_ratio': [0.0, 0.0], - 'profit_abs': [0.0, 0.0], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', ], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', ], utc=True), - 'trade_duration': [235, 40], - 'is_open': [False, False], - 'stake_amount': [0.01, 0.01], - 'open_rate': [0.104445, 0.10302485], - 'close_rate': [0.104969, 0.103541], - 'sell_reason': [SellType.ROI, SellType.ROI] - }), - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], - 'profit_ratio': [0.03, 0.01, 0.1], - 'profit_abs': [0.01, 0.02, 0.2], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', - '2018-01-30 05:30:00'], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', - '2018-01-30 08:30:00'], utc=True), - 'trade_duration': [47, 40, 20], - 'is_open': [False, False, False], - 'stake_amount': [0.01, 0.01, 0.01], - 'open_rate': [0.104445, 0.10302485, 0.122541], - 'close_rate': [0.104969, 0.103541, 0.123541], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - }), + { + 'results': result1, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }, + { + 'results': result2, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + } ]) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) From 4c00d4496de63939443f5787625e5dfa6632bcb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Apr 2021 07:40:07 +0200 Subject: [PATCH 291/460] Update tests to reflect new backtest returns --- tests/optimize/test_backtest_detail.py | 3 +- tests/optimize/test_backtesting.py | 13 +++++++- tests/optimize/test_hyperopt.py | 45 ++++++++++++++------------ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3655b941d..7da7709c6 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -501,13 +501,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: # Dummy data as we mock the analyze functions data_processed = {pair: frame.copy()} min_date, max_date = get_timerange({pair: frame}) - results = backtesting.backtest( + result = backtesting.backtest( processed=data_processed, start_date=min_date, end_date=max_date, max_open_trades=10, ) + results = result['results'] assert len(results) == len(data.trades) assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index e09f02c66..5dfc9dbc3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -804,6 +804,12 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) backtestmock = MagicMock(return_value={ 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), @@ -872,7 +878,12 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): - + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], 'profit_ratio': [0.0, 0.0], diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a80f3dced..c65e90237 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -590,26 +590,31 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'hyperopt_min_trades': 1, }) - backtest_result = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "trade_duration": [123, 34, 31, 14], - "is_open": [False, False, False, True], - "stake_amount": [0.01, 0.01, 0.01, 0.01], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - }) + backtest_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt_conf, + 'locks': [], + 'final_balance': 1000, + } mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) mocker.patch('freqtrade.optimize.hyperopt.get_timerange', From 97478abb9da71fbff2223cfddd8ca03765618834 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 09:05:46 +0200 Subject: [PATCH 292/460] Move format explanation string to HyperoptTools --- freqtrade/optimize/hyperopt.py | 17 +---------------- freqtrade/optimize/hyperopt_tools.py | 16 ++++++++++++++++ tests/optimize/test_hyperopt.py | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 229b35dd5..b69e0781a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,7 +4,6 @@ This module contains the hyperopt logic """ -import locale import logging import random import warnings @@ -301,7 +300,7 @@ class Hyperopt: strat_stats = generate_strategy_stats(processed, '', backtesting_results, min_date, max_date, market_change=0) - results_explanation = self._format_results_explanation_string( + results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) trade_count = strat_stats['total_trades'] @@ -326,20 +325,6 @@ class Hyperopt: 'total_profit': total_profit, } - def _format_results_explanation_string(self, results_metrics: Dict, stake_currency: str) -> str: - """ - Return the formatted results explanation in a string - """ - return (f"{results_metrics['total_trades']:6d} trades. " - f"{results_metrics['wins']}/{results_metrics['draws']}" - f"/{results_metrics['losses']} Wins/Draws/Losses. " - f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " - f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " - f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " - f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['holding_avg']} min." - ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') - def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( dimensions, diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 1635c0588..98df32c36 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,5 +1,6 @@ import io +import locale import logging from collections import OrderedDict from pathlib import Path @@ -145,6 +146,21 @@ class HyperoptTools(): def is_best_loss(results, current_best_loss: float) -> bool: return results['loss'] < current_best_loss + @staticmethod + def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str: + """ + Return the formatted results explanation in a string + """ + return (f"{results_metrics['total_trades']:6d} trades. " + f"{results_metrics['wins']}/{results_metrics['draws']}" + f"/{results_metrics['losses']} Wins/Draws/Losses. " + f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " + f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " + f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"Avg duration {results_metrics['holding_avg']} min." + ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') + @staticmethod def _format_explanation_string(results, total_epochs) -> str: return (("*" if results['is_initial_point'] else " ") + diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c65e90237..142fc5728 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -468,7 +468,7 @@ def test_hyperopt_format_results(hyperopt): Arrow(2017, 11, 14, 19, 32, 00), Arrow(2017, 12, 14, 19, 32, 00), market_change=0) - results_explanation = hyperopt._format_results_explanation_string(results_metrics, 'BTC') + results_explanation = HyperoptTools.format_results_explanation_string(results_metrics, 'BTC') total_profit = results_metrics['profit_total_abs'] results = { From 420e75af65303d2962c37edb5a3bbd987b0f54b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 11:24:32 +0200 Subject: [PATCH 293/460] Extract show_backtest_result for one strategy --- freqtrade/optimize/optimize_reports.py | 60 ++++++++++++++------------ 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ef5426c9a..9a60c7c4b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -567,37 +567,43 @@ def text_table_add_metrics(strat_results: Dict) -> str: return message +def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str): + """ + Print results for one strategy + """ + # Print results + print(f"Result for strategy {strategy}") + table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) + if isinstance(table, str): + print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], + stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_add_metrics(results) + if isinstance(table, str) and len(table) > 0: + print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) + print(table) + + if isinstance(table, str) and len(table) > 0: + print('=' * len(table.splitlines()[0])) + print() + + def show_backtest_results(config: Dict, backtest_stats: Dict): stake_currency = config['stake_currency'] for strategy, results in backtest_stats['strategy'].items(): - - # Print results - print(f"Result for strategy {strategy}") - table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) - if isinstance(table, str): - print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], - stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = text_table_add_metrics(results) - if isinstance(table, str) and len(table) > 0: - print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) - print(table) - - if isinstance(table, str) and len(table) > 0: - print('=' * len(table.splitlines()[0])) - print() + show_backtest_result(strategy, results, stake_currency) if len(backtest_stats['strategy']) > 1: # Print Strategy summary table From 881cba336a10e69741ab9a4c704f24b568f8b927 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 13:32:34 +0200 Subject: [PATCH 294/460] Show backtesting result in hyperopt-show --- freqtrade/commands/hyperopt_commands.py | 7 +++++++ freqtrade/optimize/hyperopt.py | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 268e3eeef..322729547 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -7,6 +7,7 @@ from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file from freqtrade.exceptions import OperationalException +from freqtrade.optimize.optimize_reports import show_backtest_result from freqtrade.state import RunMode @@ -125,6 +126,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] + + metrics = val['results_metrics'] + if 'strategy_name' in metrics: + show_backtest_result(metrics['strategy_name'], metrics, + metrics['stake_currency']) + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, header_str="Epoch details") diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b69e0781a..861d1872f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -298,8 +298,10 @@ class Hyperopt: def _get_results_dict(self, backtesting_results, min_date, max_date, params_dict, params_details, processed: Dict[str, DataFrame]): - strat_stats = generate_strategy_stats(processed, '', backtesting_results, - min_date, max_date, market_change=0) + strat_stats = generate_strategy_stats( + processed, self.backtesting.strategy.get_strategy_name(), + backtesting_results, min_date, max_date, market_change=0 + ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) From ecdfb6e5ed68bcec6f91e2b3b8dcf8f13603aa20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 13:32:53 +0200 Subject: [PATCH 295/460] Fix output of % for new format --- freqtrade/optimize/hyperopt_tools.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 98df32c36..0fdd07d83 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -157,7 +157,7 @@ class HyperoptTools(): f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " - f"({results_metrics['profit_total']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " + f"({results_metrics['profit_total'] * 100: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " f"Avg duration {results_metrics['holding_avg']} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') @@ -184,8 +184,10 @@ class HyperoptTools(): if 'results_metrics.winsdrawslosses' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + legacy_mode = True if 'results_metrics.total_trades' in trials: + legacy_mode = False # New mode, using backtest result for metrics trials['results_metrics.winsdrawslosses'] = trials.apply( lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " @@ -212,12 +214,12 @@ class HyperoptTools(): trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Trades'] = trials['Trades'].astype(str) - + perc_multi = 1 if legacy_mode else 100 trials['Epoch'] = trials['Epoch'].apply( lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: f'{x:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x * perc_multi:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') ) trials['Avg duration'] = trials['Avg duration'].apply( lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" @@ -231,7 +233,7 @@ class HyperoptTools(): trials['Profit'] = trials.apply( lambda x: '{} {}'.format( round_coin_value(x['Total profit'], stake_currency), - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') + '({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ') ).rjust(25+len(stake_currency)) if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), axis=1 From ced5cc7ce21f5e79196bb3865baff1908c2e936f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 May 2021 13:33:12 +0200 Subject: [PATCH 296/460] Don't recalculate min/max date - they won't change between epochs --- freqtrade/optimize/hyperopt.py | 16 +++++++--------- tests/optimize/test_hyperopt.py | 2 ++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 861d1872f..d6c5cbf93 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -275,12 +275,10 @@ class Hyperopt: processed = load(self.data_pickle_file) - min_date, max_date = get_timerange(processed) - bt_results = self.backtesting.backtest( processed=processed, - start_date=min_date.datetime, - end_date=max_date.datetime, + start_date=self.min_date.datetime, + end_date=self.max_date.datetime, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, enable_protections=self.config.get('enable_protections', False), @@ -291,7 +289,7 @@ class Hyperopt: 'backtest_end_time': int(backtest_end_time.timestamp()), }) - return self._get_results_dict(bt_results, min_date, max_date, + return self._get_results_dict(bt_results, self.min_date, self.max_date, params_dict, params_details, processed=processed) @@ -357,11 +355,11 @@ class Hyperopt: for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange, startup_candles=self.backtesting.required_startup) - min_date, max_date = get_timerange(preprocessed) + self.min_date, self.max_date = get_timerange(preprocessed) - logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(self.max_date - self.min_date).days} days)..') dump(preprocessed, self.data_pickle_file) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 142fc5728..554f525a8 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -693,6 +693,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: } hyperopt = Hyperopt(hyperopt_conf) + hyperopt.min_date = Arrow(2017, 12, 10) + hyperopt.max_date = Arrow(2017, 12, 13) hyperopt.dimensions = hyperopt.hyperopt_space() generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected From 46f0f6603990194b2c5dd1195b0fdb01975ef914 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 09:35:10 +0200 Subject: [PATCH 297/460] Keep dimensions stored in hyperopt class There is no point in regenerating them and it will cause some overhead as all space classes will be recreated for every epoch. --- freqtrade/optimize/hyperopt.py | 63 ++++++++++++++++----------------- tests/optimize/test_hyperopt.py | 3 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d6c5cbf93..4e32c2ecb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -65,6 +65,13 @@ class Hyperopt: custom_hyperopt: IHyperOpt def __init__(self, config: Dict[str, Any]) -> None: + self.buy_space: List[Dimension] = [] + self.sell_space: List[Dimension] = [] + self.roi_space: List[Dimension] = [] + self.stoploss_space: List[Dimension] = [] + self.trailing_space: List[Dimension] = [] + self.dimensions: List[Dimension] = [] + self.config = config self.backtesting = Backtesting(self.config) @@ -139,9 +146,7 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() - def _get_params_dict(self, raw_params: List[Any]) -> Dict: - - dimensions: List[Dimension] = self.dimensions + def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: # Ensure the number of dimensions match # the number of parameters in the list. @@ -175,16 +180,13 @@ class Hyperopt: result: Dict = {} if HyperoptTools.has_space(self.config, 'buy'): - result['buy'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('buy')} + result['buy'] = {p.name: params.get(p.name) for p in self.buy_space} if HyperoptTools.has_space(self.config, 'sell'): - result['sell'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('sell')} + result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} if HyperoptTools.has_space(self.config, 'roi'): result['roi'] = self.custom_hyperopt.generate_roi_table(params) if HyperoptTools.has_space(self.config, 'stoploss'): - result['stoploss'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('stoploss')} + result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} if HyperoptTools.has_space(self.config, 'trailing'): result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) @@ -207,38 +209,32 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: + def init_spaces(self): """ - Return the dimensions in the hyperoptimization space. - :param space: Defines hyperspace to return dimensions for. - If None, then the self.has_space() will be used to return dimensions - for all hyperspaces used. + Assign the dimensions in the hyperoptimization space. """ - spaces: List[Dimension] = [] - if space == 'buy' or (space is None and HyperoptTools.has_space(self.config, 'buy')): + if HyperoptTools.has_space(self.config, 'buy'): logger.debug("Hyperopt has 'buy' space") - spaces += self.custom_hyperopt.indicator_space() + self.buy_space = self.custom_hyperopt.indicator_space() - if space == 'sell' or (space is None and HyperoptTools.has_space(self.config, 'sell')): + if HyperoptTools.has_space(self.config, 'sell'): logger.debug("Hyperopt has 'sell' space") - spaces += self.custom_hyperopt.sell_indicator_space() + self.sell_space = self.custom_hyperopt.sell_indicator_space() - if space == 'roi' or (space is None and HyperoptTools.has_space(self.config, 'roi')): + if HyperoptTools.has_space(self.config, 'roi'): logger.debug("Hyperopt has 'roi' space") - spaces += self.custom_hyperopt.roi_space() + self.roi_space = self.custom_hyperopt.roi_space() - if space == 'stoploss' or (space is None - and HyperoptTools.has_space(self.config, 'stoploss')): + if HyperoptTools.has_space(self.config, 'stoploss'): logger.debug("Hyperopt has 'stoploss' space") - spaces += self.custom_hyperopt.stoploss_space() + self.stoploss_space = self.custom_hyperopt.stoploss_space() - if space == 'trailing' or (space is None - and HyperoptTools.has_space(self.config, 'trailing')): + if HyperoptTools.has_space(self.config, 'trailing'): logger.debug("Hyperopt has 'trailing' space") - spaces += self.custom_hyperopt.trailing_space() - - return spaces + self.trailing_space = self.custom_hyperopt.trailing_space() + self.dimensions = (self.buy_space + self.sell_space + self.roi_space + + self.stoploss_space + self.trailing_space) def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: """ @@ -246,9 +242,10 @@ class Hyperopt: Keep this function as optimized as possible! """ backtest_start_time = datetime.now(timezone.utc) - params_dict = self._get_params_dict(raw_params) + params_dict = self._get_params_dict(self.dimensions, raw_params) params_details = self._get_params_details(params_dict) + # Apply parameters if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) @@ -294,7 +291,8 @@ class Hyperopt: processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict[str, DataFrame]): + params_dict, params_details, processed: Dict[str, DataFrame] + ) -> Dict[str, Any]: strat_stats = generate_strategy_stats( processed, self.backtesting.strategy.get_strategy_name(), @@ -347,6 +345,8 @@ class Hyperopt: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 + # Initialize spaces ... + self.init_spaces() data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) @@ -377,7 +377,6 @@ class Hyperopt: config_jobs = self.config.get('hyperopt_jobs', -1) logger.info(f'Number of parallel jobs set as: {config_jobs}') - self.dimensions: List[Dimension] = self.hyperopt_space() self.opt = self.get_optimizer(self.dimensions, config_jobs) if self.print_colorized: diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 554f525a8..82bb4fdd2 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -695,7 +695,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt = Hyperopt(hyperopt_conf) hyperopt.min_date = Arrow(2017, 12, 10) hyperopt.max_date = Arrow(2017, 12, 13) - hyperopt.dimensions = hyperopt.hyperopt_space() + hyperopt.init_spaces() + hyperopt.dimensions = hyperopt.dimensions generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected From 6b6270db132ebb41dc334aaf8076588c3f368efa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 10:37:54 +0200 Subject: [PATCH 298/460] Add hint about "sell_profit_only" to docs --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7fa889dd1..eb322df9d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -51,7 +51,7 @@ For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. !!! Note - Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. + Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: From 9049d6b779c39a697e755e5e69c48c3eb1994d48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 10:45:21 +0200 Subject: [PATCH 299/460] Reformat hyper to cache parameters --- freqtrade/strategy/hyper.py | 43 +++++++++++++++++++++++++++++--- tests/strategy/test_interface.py | 2 +- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 2714ffb43..7dee47d87 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -5,7 +5,7 @@ This module defines a base class for auto-hyperoptable strategies. import logging from abc import ABC, abstractmethod from contextlib import suppress -from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, Union from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -29,6 +29,7 @@ class BaseParameter(ABC): default: Any value: Any in_space: bool = False + name: str def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -250,6 +251,9 @@ class HyperStrategyMixin(object): Initialize hyperoptable strategy mixin. """ self.config = config + self.ft_buy_params: List[BaseParameter] = [] + self.ft_sell_params: List[BaseParameter] = [] + self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: @@ -260,15 +264,26 @@ class HyperStrategyMixin(object): """ if category not in ('buy', 'sell', None): raise OperationalException('Category must be one of: "buy", "sell", None.') + + if category is None: + params = self.ft_buy_params + self.ft_sell_params + else: + params = getattr(self, f"ft_{category}_params") + + for par in params: + yield par.name, par + + def _detect_parameters(self, category: str) -> Iterator[Tuple[str, BaseParameter]]: + """ Detect all parameters for 'category' """ for attr_name in dir(self): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): - if (category and attr_name.startswith(category + '_') + if (attr_name.startswith(category + '_') and attr.category is not None and attr.category != category): raise OperationalException( f'Inconclusive parameter name {attr_name}, category: {attr.category}.') - if (category is None or category == attr.category or + if (category == attr.category or (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr @@ -286,9 +301,16 @@ class HyperStrategyMixin(object): """ if not params: logger.info(f"No params for {space} found, using default values.") + param_container: List[BaseParameter] = getattr(self, f"ft_{space}_params") - for attr_name, attr in self.enumerate_parameters(space): + for attr_name, attr in self._detect_parameters(space): + attr.name = attr_name attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) + if not attr.category: + attr.category = space + + param_container.append(attr) + if params and attr_name in params: if attr.load: attr.value = params[attr_name] @@ -298,3 +320,16 @@ class HyperStrategyMixin(object): f'Default value "{attr.value}" used.') else: logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') + + def get_params_dict(self): + """ + Returns list of Parameters that are not part of the current optimize job + """ + params = { + 'buy': {}, + 'sell': {} + } + for name, p in self.enumerate_parameters(): + if not p.optimize or not p.in_space: + params[p.category][name] = p.value + return params diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4ca514349..a241d7f43 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -671,4 +671,4 @@ def test_auto_hyperopt_interface(default_conf): strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): - [x for x in strategy.enumerate_parameters('sell')] + [x for x in strategy._detect_parameters('sell')] From 8ee0b0d8e85228cdade883066edef1d29af2a0c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 10:46:04 +0200 Subject: [PATCH 300/460] Store not optimized parameters (if applicable) --- freqtrade/optimize/hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4e32c2ecb..ec3c6f385 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -301,6 +301,8 @@ class Hyperopt: results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) + not_optimized = self.backtesting.strategy.get_params_dict() + trade_count = strat_stats['total_trades'] total_profit = strat_stats['profit_total'] @@ -318,6 +320,7 @@ class Hyperopt: 'loss': loss, 'params_dict': params_dict, 'params_details': params_details, + 'params_not_optimized': not_optimized, 'results_metrics': strat_stats, 'results_explanation': results_explanation, 'total_profit': total_profit, From d069ad43d83eae4215409e68fd3cc8626fb32983 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 11:01:26 +0200 Subject: [PATCH 301/460] Small reformatting in hyperopt --- freqtrade/optimize/hyperopt.py | 6 +++--- tests/optimize/test_hyperopt.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ec3c6f385..639e9fb93 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -243,7 +243,6 @@ class Hyperopt: """ backtest_start_time = datetime.now(timezone.utc) params_dict = self._get_params_dict(self.dimensions, raw_params) - params_details = self._get_params_details(params_dict) # Apply parameters if HyperoptTools.has_space(self.config, 'roi'): @@ -287,12 +286,13 @@ class Hyperopt: }) return self._get_results_dict(bt_results, self.min_date, self.max_date, - params_dict, params_details, + params_dict, processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict[str, DataFrame] + params_dict, processed: Dict[str, DataFrame] ) -> Dict[str, Any]: + params_details = self._get_params_details(params_dict) strat_stats = generate_strategy_stats( processed, self.backtesting.strategy.get_strategy_name(), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 82bb4fdd2..774dd35a4 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -688,6 +688,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, + 'params_not_optimized': {'buy': {}, 'sell': {}}, 'results_metrics': ANY, 'total_profit': 3.1e-08 } From 287b43e999b89da122e052014b2b0fd53297de7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 11:30:53 +0200 Subject: [PATCH 302/460] Output strategy results including non-optimized parameters --- freqtrade/optimize/hyperopt_tools.py | 54 +++++++++++++++++++--------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 0fdd07d83..724f451c5 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -4,7 +4,6 @@ import locale import logging from collections import OrderedDict from pathlib import Path -from pprint import pformat from typing import Any, Dict, List import rapidjson @@ -66,6 +65,7 @@ class HyperoptTools(): Display details of the hyperopt result """ params = results.get('params_details', {}) + non_optimized = results.get('params_not_optimized', {}) # Default header string if header_str is None: @@ -82,8 +82,10 @@ class HyperoptTools(): print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: - HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") - HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:") + HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", + non_optimized) + HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:", + non_optimized) HyperoptTools._params_pretty_print(params, 'roi', "ROI table:") HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") @@ -109,12 +111,12 @@ class HyperoptTools(): result_dict.update(space_params) @staticmethod - def _params_pretty_print(params, space: str, header: str) -> None: - if space in params: + def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: + if space in params or space in non_optimized: space_params = HyperoptTools._space_params(params, space, 5) - params_result = f"\n# {header}\n" + result = f"\n# {header}\n" if space == 'stoploss': - params_result += f"stoploss = {space_params.get('stoploss')}" + result += f"stoploss = {space_params.get('stoploss')}" elif space == 'roi': # TODO: get rid of OrderedDict when support for python 3.6 will be # dropped (dicts keep the order as the language feature) @@ -123,24 +125,44 @@ class HyperoptTools(): (str(k), v) for k, v in space_params.items() ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" + result += f"minimal_roi = {minimal_roi_result}" elif space == 'trailing': for k, v in space_params.items(): - params_result += f'{k} = {v}\n' + result += f'{k} = {v}\n' else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + no_params = HyperoptTools._space_params(non_optimized, space, 5) - params_result = params_result.replace("\n", "\n ") - print(params_result) + result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}" + + result = result.replace("\n", "\n ") + print(result) @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d + d = params.get(space) + if d: + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + return {} + + @staticmethod + def _pprint(params, non_optimized, indent: int = 4): + """ + Pretty-print hyperopt results (based on 2 dicts - with add. comment) + """ + p = params.copy() + p.update(non_optimized) + result = '{\n' + + for k, param in p.items(): + result += " " * indent + f'"{k}": {param},' + if k in non_optimized: + result += " # value loaded from strategy" + result += "\n" + result += '}' + return result @staticmethod def is_best_loss(results, current_best_loss: float) -> bool: From 4fc37f15d106855b1bc945d33b2ce5119ea8f9fe Mon Sep 17 00:00:00 2001 From: youpas Date: Sun, 2 May 2021 11:41:26 +0200 Subject: [PATCH 303/460] Fixed syntax error in the example Removed extra comma in the "Full example of Pairlist Handlers" section. --- 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 85d157e75..2cd6feca0 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -204,7 +204,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, { "method": "VolumePairList", "number_assets": 20, - "sort_key": "quoteVolume", + "sort_key": "quoteVolume" }, {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, From b71d8505965624a308d6576a52892a6d7d17494f Mon Sep 17 00:00:00 2001 From: youpas Date: Sun, 2 May 2021 11:47:02 +0200 Subject: [PATCH 304/460] Fixed anchor link for PriceFilter --- 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 85d157e75..d653303f6 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -193,7 +193,7 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove ### Full example of Pairlist Handlers -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. ```json "exchange": { From 99e1ef9b4a427b28953b35ae273f448bc60ef6be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 19:21:26 +0200 Subject: [PATCH 305/460] Fix docs typo for CategoryParameter closes #4852 --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b3fdc699b..5f1f9bffa 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -251,9 +251,9 @@ We continue to define hyperoptable parameters: class MyAwesomeStrategy(IStrategy): buy_adx = IntParameter(20, 40, default=30) buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]), - buy_rsi_enabled = CategoricalParameter([True, False]), - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), + buy_adx_enabled = CategoricalParameter([True, False]) + buy_rsi_enabled = CategoricalParameter([True, False]) + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']) ``` Above definition says: I have five parameters I want to randomly combine to find the best combination. From ef9dd0676cf0de365967d4e0108162957dc100bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:06:47 +0200 Subject: [PATCH 306/460] Rename hyperoptresult fixture to avoid naming collision --- tests/commands/test_commands.py | 8 ++++---- tests/conftest.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d86bced5d..9195a1a92 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,10 +918,10 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) + MagicMock(return_value=saved_hyperopt_results) ) args = [ @@ -1150,10 +1150,10 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): f.unlink() -def test_hyperopt_show(mocker, capsys, hyperopt_results): +def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) + MagicMock(return_value=saved_hyperopt_results) ) args = [ diff --git a/tests/conftest.py b/tests/conftest.py index 788586134..ffb09cc60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1778,7 +1778,7 @@ def open_trade(): @pytest.fixture -def hyperopt_results(): +def saved_hyperopt_results(): return [ { 'loss': 0.4366182531161519, From 303895b33e3a0931dd4dc26c687157a5fa080a9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:07:22 +0200 Subject: [PATCH 307/460] Add support for filters to new hyperopt-results --- freqtrade/commands/hyperopt_commands.py | 71 ++++++++++++++++++------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 322729547..cf3bc3005 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -139,11 +139,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: """ Filter our items from the list of hyperopt results + TODO: after 2021.5 remove all "legacy" mode queries. """ if filteroptions['only_best']: epochs = [x for x in epochs if x['is_best']] if filteroptions['only_profitable']: - epochs = [x for x in epochs if x['results_metrics']['profit'] > 0] + epochs = [x for x in epochs if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0)) > 0] epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) @@ -160,34 +162,55 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: return epochs +def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): + """ + Filter epochs with trade-counts > trades + """ + return [ + x for x in epochs + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades', 0) + ) > trade_count + ] + + def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_trades'] > 0: - epochs = [ - x for x in epochs - if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] - ] + epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) + if filteroptions['filter_max_trades'] > 0: epochs = [ x for x in epochs - if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades') + ) < filteroptions['filter_max_trades'] ] return epochs def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: + def get_duration_value(x): + # Duration in minutes ... + if 'duration' in x['results_metrics']: + return x['results_metrics']['duration'] + else: + # New mode + avg = x['results_metrics']['holding_avg'] + return avg.total_seconds() // 60 + if filteroptions['filter_min_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time'] + if get_duration_value(x) > filteroptions['filter_min_avg_time'] ] if filteroptions['filter_max_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] + if get_duration_value(x) < filteroptions['filter_max_avg_time'] ] return epochs @@ -196,28 +219,36 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] > filteroptions['filter_min_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) + ) > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] < filteroptions['filter_max_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) + ) < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0) + ) > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0) + ) < filteroptions['filter_max_total_profit'] ] return epochs @@ -225,11 +256,11 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] if filteroptions['filter_max_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] From fc110ea418719661052616378adb882463e3ff0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:41:45 +0200 Subject: [PATCH 308/460] Support csv export for new and old versions --- freqtrade/commands/hyperopt_commands.py | 8 +- freqtrade/optimize/hyperopt_tools.py | 33 ++++-- tests/conftest.py | 137 +++++++++++++++++++++++- 3 files changed, 162 insertions(+), 16 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index cf3bc3005..e072e12cb 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -223,7 +223,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 ) > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: @@ -231,7 +231,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'avg_profit', x['results_metrics'].get('profit_mean', 0) + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 ) < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: @@ -239,7 +239,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total', 0) + 'profit', x['results_metrics'].get('profit_total_abs', 0) ) > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: @@ -247,7 +247,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: epochs = [ x for x in epochs if x['results_metrics'].get( - 'profit', x['results_metrics'].get('profit_total', 0) + 'profit', x['results_metrics'].get('profit_total_abs', 0) ) < filteroptions['filter_max_total_profit'] ] return epochs diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 724f451c5..ee3366a4e 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -318,11 +318,21 @@ class HyperoptTools(): trials['Best'] = '' trials['Stake currency'] = config['stake_currency'] - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] + if 'results_metrics.total_trades' in trials: + base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.profit_mean', 'results_metrics.profit_median', + 'results_metrics.profit_total', + 'Stake currency', + 'results_metrics.profit_total_abs', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best'] + perc_multi = 100 + else: + perc_multi = 1 + base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', + 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] @@ -339,21 +349,24 @@ class HyperoptTools(): trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Epoch'] = trials['Epoch'].astype(str) trials['Trades'] = trials['Trades'].astype(str) + trials['Median profit'] = trials['Median profit'] * perc_multi trials['Total profit'] = trials['Total profit'].apply( - lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" + lambda x: f'{x:,.8f}' if x != 0.0 else "" ) trials['Profit'] = trials['Profit'].apply( - lambda x: '{:,.2f}'.format(x) if not isna(x) else "" + lambda x: f'{x:,.2f}' if not isna(x) else "" ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" + lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" ) trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" + lambda x: f'{x:,.1f} m' if isinstance( + x, float) else f"{x.total_seconds() // 60:,.1f} m" + if not isna(x) else "" ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x) if x != 100000 else "" + lambda x: f'{x:,.5f}' if x != 100000 else "" ) trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) diff --git a/tests/conftest.py b/tests/conftest.py index ffb09cc60..83a34359a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import json import logging import re from copy import deepcopy -from datetime import datetime +from datetime import datetime, timedelta from functools import reduce from pathlib import Path from unittest.mock import MagicMock, Mock, PropertyMock @@ -1778,7 +1778,7 @@ def open_trade(): @pytest.fixture -def saved_hyperopt_results(): +def saved_hyperopt_results_legacy(): return [ { 'loss': 0.4366182531161519, @@ -1907,3 +1907,136 @@ def saved_hyperopt_results(): 'is_best': False } ] + + +@pytest.fixture +def saved_hyperopt_results(): + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + # New Hyperopt mode! + 'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] From 6d7096dc665a7ffb0818ab31bba180e983307238 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 May 2021 20:42:01 +0200 Subject: [PATCH 309/460] Use both old and new fixtures for test --- tests/commands/test_commands.py | 459 ++++++++++++++++---------------- 1 file changed, 230 insertions(+), 229 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 9195a1a92..60e6d2ea8 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,236 +918,237 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results): - mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=saved_hyperopt_results) - ) +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, saved_hyperopt_results_legacy): + for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', + MagicMock(return_value=saved_hyperopt_results_legacy) + ) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", - " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--best", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 5/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", - "Sell hyperspace params", "ROI table", "Stoploss"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--max-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-profit", "0.11", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-profit", "0.10", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--max-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-time", "2000", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", - " 8/12", " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-time", "1500", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 6/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" - " 9/12", " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--export-csv", "test_file.csv", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - log_has("CSV file created: test_file.csv", caplog) - f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() - assert f.is_file() - f.unlink() + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", + " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--best", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 5/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", + "Sell hyperspace params", "ROI table", "Stoploss"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--max-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-profit", "0.11", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-profit", "0.10", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-time", "2000", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", + " 8/12", " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-time", "1500", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 6/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" + " 9/12", " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--export-csv", "test_file.csv", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + log_has("CSV file created: test_file.csv", caplog) + f = Path("test_file.csv") + assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() + assert f.is_file() + f.unlink() def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): From da574e4e69b9afa6f76484bd09738dde2bd97ee7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 May 2021 06:30:41 +0200 Subject: [PATCH 310/460] Small style fixes --- freqtrade/optimize/hyperopt_tools.py | 3 +-- tests/commands/test_commands.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ee3366a4e..f655582c4 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -362,8 +362,7 @@ class HyperoptTools(): ) trials['Avg duration'] = trials['Avg duration'].apply( lambda x: f'{x:,.1f} m' if isinstance( - x, float) else f"{x.total_seconds() // 60:,.1f} m" - if not isna(x) else "" + x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else "" ) trials['Objective'] = trials['Objective'].apply( lambda x: f'{x:,.5f}' if x != 100000 else "" diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 60e6d2ea8..4d3937d87 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,7 +918,8 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, saved_hyperopt_results_legacy): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, + saved_hyperopt_results_legacy): for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', From 8364343cd63853eaa878dd4621778ee1669a918e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:08 +0000 Subject: [PATCH 311/460] Bump scikit-learn from 0.24.1 to 0.24.2 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.24.1 to 0.24.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.24.1...0.24.2) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 79ad722e2..5e7e9d9d2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.6.3 -scikit-learn==0.24.1 +scikit-learn==0.24.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.1 From 8ed15fb7ccb298130393af64ce10be1d96bfb39b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:18 +0000 Subject: [PATCH 312/460] Bump sqlalchemy from 1.4.11 to 1.4.12 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.11 to 1.4.12. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..6b537f5e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.48.76 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.11 +SQLAlchemy==1.4.12 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 37227170b36bee2e13d062c93736ceb559795eda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:30 +0000 Subject: [PATCH 313/460] Bump pyjwt from 2.0.1 to 2.1.0 Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.0.1 to 2.1.0. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/compare/2.0.1...2.1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..04a3c4ad8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ sdnotify==0.3.2 # API Server fastapi==0.63.0 uvicorn==0.13.4 -pyjwt==2.0.1 +pyjwt==2.1.0 aiofiles==0.6.0 # Support for colorized terminal output From cea207026aab9753e8378c3da94e8491ce3feb3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:26:39 +0000 Subject: [PATCH 314/460] Bump technical from 1.2.2 to 1.3.0 Bumps [technical](https://github.com/freqtrade/technical) from 1.2.2 to 1.3.0. - [Release notes](https://github.com/freqtrade/technical/releases) - [Commits](https://github.com/freqtrade/technical/compare/1.2.2...1.3.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..6dcefcd2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 -technical==1.2.2 +technical==1.3.0 tabulate==0.8.9 pycoingecko==2.0.0 jinja2==2.11.3 From a63d9e9515c2855c310b5fcceebcf8e11e64ea3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 05:27:17 +0000 Subject: [PATCH 315/460] Bump arrow from 1.0.3 to 1.1.0 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.0.3 to 1.1.0. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.0.3...1.1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f1739a1..86a39c513 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.11 python-telegram-bot==13.4.1 -arrow==1.0.3 +arrow==1.1.0 cachetools==4.2.1 requests==2.25.1 urllib3==1.26.4 From f138cca7975cd2cdfb6f539ba828fe04c47c0a67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 May 2021 08:33:06 +0200 Subject: [PATCH 316/460] Be explicit with space assignment in documentation --- docs/hyperopt.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5f1f9bffa..d8f4a8071 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -249,11 +249,11 @@ We continue to define hyperoptable parameters: ```python class MyAwesomeStrategy(IStrategy): - buy_adx = IntParameter(20, 40, default=30) - buy_rsi = IntParameter(20, 40, default=30) - buy_adx_enabled = CategoricalParameter([True, False]) - buy_rsi_enabled = CategoricalParameter([True, False]) - buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']) + buy_adx = IntParameter(20, 40, default=30, space="buy") + buy_rsi = IntParameter(20, 40, default=30, space="buy") + buy_adx_enabled = CategoricalParameter([True, False], space="buy") + buy_rsi_enabled = CategoricalParameter([True, False], space="buy") + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal'], space="buy") ``` Above definition says: I have five parameters I want to randomly combine to find the best combination. @@ -262,6 +262,10 @@ Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. +!!! Note "Parameter space assignment" + Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly. + If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. + So let's write the buy strategy using these values: ```python From 82a08bd7def9d1661a3804c32960abe630d1626f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 10:16:58 +0000 Subject: [PATCH 317/460] Bump python-telegram-bot from 13.4.1 to 13.5 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.4.1 to 13.5. - [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.4.1...v13.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91d760c9e..d4fe5bdd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.48.76 cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.12 -python-telegram-bot==13.4.1 +python-telegram-bot==13.5 arrow==1.1.0 cachetools==4.2.1 requests==2.25.1 From 2d8982426711e8a11a256c00121bccca34dfa664 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 10:21:49 +0000 Subject: [PATCH 318/460] Bump cachetools from 4.2.1 to 4.2.2 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.2.1 to 4.2.2. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v4.2.1...v4.2.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91d760c9e..08e13525d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiohttp==3.7.4.post0 SQLAlchemy==1.4.12 python-telegram-bot==13.4.1 arrow==1.1.0 -cachetools==4.2.1 +cachetools==4.2.2 requests==2.25.1 urllib3==1.26.4 wrapt==1.12.1 From 860379bc58262f69735aceb018850476abcf0db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 May 2021 10:23:52 +0000 Subject: [PATCH 319/460] Bump ccxt from 1.48.76 to 1.49.30 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.48.76 to 1.49.30. - [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.48.76...1.49.30) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 91d760c9e..d4f89be5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.48.76 +ccxt==1.49.30 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From da47f4e1a46a4c4982f3b1e67f39eb30516c9c59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 06:47:26 +0200 Subject: [PATCH 320/460] Fix Kraken balance update error closes #4873 --- freqtrade/exchange/kraken.py | 2 ++ tests/exchange/test_kraken.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 786f1b592..6f1fa409a 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -53,6 +53,8 @@ class Kraken(Exchange): # x["side"], x["amount"], ) for x in orders] for bal in balances: + if not isinstance(balances[bal], dict): + continue balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal) balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 97f428e2f..ed22cde92 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -90,6 +90,7 @@ def test_get_balances_prod(default_conf, mocker): '3ST': balance_item.copy(), '4ST': balance_item.copy(), 'EUR': balance_item.copy(), + 'timestamp': 123123 }) kraken_open_orders = [{'symbol': '1ST/EUR', 'type': 'limit', @@ -138,7 +139,7 @@ def test_get_balances_prod(default_conf, mocker): default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") balances = exchange.get_balances() - assert len(balances) == 5 + assert len(balances) == 6 assert balances['1ST']['free'] == 9.0 assert balances['1ST']['total'] == 10.0 From f55ce04fa6a8aa41cdfc66c0c3cfcd1cd5adde46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 May 2021 05:14:42 +0000 Subject: [PATCH 321/460] Bump python from 3.9.4-slim-buster to 3.9.5-slim-buster Bumps python from 3.9.4-slim-buster to 3.9.5-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 128d0e19c..7d5afac9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.4-slim-buster as base +FROM python:3.9.5-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 2b3bca042..9b825d126 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.5-slim-buster as base # Setup env ENV LANG C.UTF-8 From 947ad856c066961da956ebc2682c0b12a4a6a765 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 08:12:28 +0200 Subject: [PATCH 322/460] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 9b825d126..2b3bca042 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.5-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From 431cb5313f7c191b70e895faf4c422db310654f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 07:37:21 +0200 Subject: [PATCH 323/460] Support informative pairs in edge positioning --- freqtrade/edge/edge_positioning.py | 34 ++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 334aabfab..721e22262 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -1,6 +1,8 @@ # pragma pylint: disable=W0603 """ Edge positioning package """ import logging +from collections import defaultdict +from copy import deepcopy from typing import Any, Dict, List, NamedTuple import arrow @@ -12,8 +14,10 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.exceptions import OperationalException +from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist -from freqtrade.strategy.interface import SellType +from freqtrade.state import RunMode +from freqtrade.strategy.interface import IStrategy, SellType logger = logging.getLogger(__name__) @@ -45,7 +49,7 @@ class Edge: self.config = config self.exchange = exchange - self.strategy = strategy + self.strategy: IStrategy = strategy self.edge_config = self.config.get('edge', {}) self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs @@ -102,14 +106,33 @@ class Edge: logger.info('Using local backtesting data (using whitelist in given config) ...') if self._refresh_pairs: + timerange_startup = deepcopy(self._timerange) + timerange_startup.subtract_start(timeframe_to_seconds( + self.strategy.timeframe) * self.strategy.startup_candle_count) refresh_data( datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, timeframe=self.strategy.timeframe, - timerange=self._timerange, + timerange=timerange_startup, data_format=self.config.get('dataformat_ohlcv', 'json'), ) + # Download informative pairs too + res = defaultdict(list) + for p, t in self.strategy.informative_pairs(): + res[t].append(p) + for timeframe, inf_pairs in res.items(): + timerange_startup = deepcopy(self._timerange) + timerange_startup.subtract_start(timeframe_to_seconds( + timeframe) * self.strategy.startup_candle_count) + refresh_data( + datadir=self.config['datadir'], + pairs=inf_pairs, + exchange=self.exchange, + timeframe=timeframe, + timerange=timerange_startup, + data_format=self.config.get('dataformat_ohlcv', 'json'), + ) data = load_data( datadir=self.config['datadir'], @@ -125,8 +148,11 @@ class Edge: self._cached_pairs = {} logger.critical("No data found. Edge is stopped ...") return False - + # Fake run-mode to Edge + prior_rm = self.config['runmode'] + self.config['runmode'] = RunMode.EDGE preprocessed = self.strategy.ohlcvdata_to_dataframe(data) + self.config['runmode'] = prior_rm # Print timeframe min_date, max_date = get_timerange(preprocessed) From a710b7dc01296e2b3330da0cf845bfbf3f62f6e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 May 2021 07:46:30 +0200 Subject: [PATCH 324/460] Update tests to match new behaviour --- tests/conftest.py | 2 ++ tests/edge/test_edge.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 83a34359a..ef2bd0613 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6) @@ -1677,6 +1678,7 @@ def buy_order_fee(): @pytest.fixture(scope="function") def edge_conf(default_conf): conf = deepcopy(default_conf) + conf['runmode'] = RunMode.DRY_RUN conf['max_open_trades'] = -1 conf['tradable_balance_ratio'] = 0.5 conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 25e0da5e2..c4620e1c7 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -344,6 +344,8 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): def test_edge_process_no_pairs(mocker, edge_conf, caplog): edge_conf['exchange']['pair_whitelist'] = [] + mocker.patch('freqtrade.freqtradebot.validate_config_consistency') + freqtrade = get_patched_freqtradebot(mocker, edge_conf) fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data') From 4f529fe4246bd339bed059619c1dd57e850d857b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 19:34:10 +0200 Subject: [PATCH 325/460] Don't use Arrow to get min/max backtest dates --- freqtrade/configuration/timerange.py | 7 ++++--- freqtrade/data/history/history_utils.py | 4 ++-- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/hyperopt.py | 6 +++--- freqtrade/optimize/optimize_reports.py | 13 ++++++------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 6072e296c..6979c8cd1 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -3,6 +3,7 @@ This module contains the argument manager class """ import logging import re +from datetime import datetime from typing import Optional import arrow @@ -43,7 +44,7 @@ class TimeRange: self.startts = self.startts - seconds def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, - min_date: arrow.Arrow) -> None: + min_date: datetime) -> None: """ Adjust startts by candles. Applies only if no startup-candles have been available. @@ -54,11 +55,11 @@ class TimeRange: :return: None (Modifies the object in place) """ if (not self.starttype or (startup_candles - and min_date.int_timestamp >= self.startts)): + and min_date.timestamp() >= self.startts)): # If no startts was defined, or backtest-data starts at the defined backtest-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) - self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles) + self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles) self.starttype = 'date' @staticmethod diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 58965abe0..32c7ce7f9 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -367,7 +367,7 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], logger.exception(f'Could not convert {pair} to OHLCV.') -def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]: """ Get the maximum common timerange for the given backtest data. @@ -375,7 +375,7 @@ def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] :return: tuple containing min_date, max_date """ timeranges = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + (frame['date'].min().to_pydatetime(), frame['date'].max().to_pydatetime()) for frame in data.values() ] return (min(timeranges, key=operator.itemgetter(0))[0], diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 54e7b806f..76d76dd26 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -456,8 +456,8 @@ class Backtesting: # Execute backtest and store results results = self.backtest( processed=preprocessed, - start_date=min_date.datetime, - end_date=max_date.datetime, + start_date=min_date, + end_date=max_date, max_open_trades=max_open_trades, position_stacking=self.config.get('position_stacking', False), enable_protections=self.config.get('enable_protections', False), diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 639e9fb93..e0a6d50a0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -273,8 +273,8 @@ class Hyperopt: bt_results = self.backtesting.backtest( processed=processed, - start_date=self.min_date.datetime, - end_date=self.max_date.datetime, + start_date=self.min_date, + end_date=self.max_date, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, enable_protections=self.config.get('enable_protections', False), @@ -314,7 +314,7 @@ class Hyperopt: if trade_count >= self.config['hyperopt_min_trades']: loss = self.calculate_loss(results=backtesting_results['results'], trade_count=trade_count, - min_date=min_date.datetime, max_date=max_date.datetime, + min_date=min_date, max_date=max_date, config=self.config, processed=processed) return { 'loss': loss, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 9a60c7c4b..170e19ecc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -from arrow import Arrow from numpy import int64 from pandas import DataFrame from tabulate import tabulate @@ -259,7 +258,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: def generate_strategy_stats(btdata: Dict[str, DataFrame], strategy: str, content: Dict[str, Any], - min_date: Arrow, max_date: Arrow, + min_date: datetime, max_date: datetime, market_change: float ) -> Dict[str, Any]: """ @@ -314,10 +313,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), - 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.int_timestamp * 1000, - 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.int_timestamp * 1000, + 'backtest_start': min_date, + 'backtest_start_ts': int(min_date.timestamp() * 1000), + 'backtest_end': max_date, + 'backtest_end_ts': int(max_date.timestamp() * 1000), 'backtest_days': backtest_days, 'backtest_run_start_ts': content['backtest_start_time'], @@ -397,7 +396,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], def generate_backtest_stats(btdata: Dict[str, DataFrame], all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], - min_date: Arrow, max_date: Arrow + min_date: datetime, max_date: datetime ) -> Dict[str, Any]: """ :param btdata: Backtest data From 554f5f14b6a42366026a043d48f3790a6d3cc570 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 May 2021 20:49:48 +0200 Subject: [PATCH 326/460] Raise exception if no data is left --- freqtrade/optimize/backtesting.py | 10 ++++++---- tests/optimize/test_backtesting.py | 14 +++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 76d76dd26..899da03e4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,7 +9,7 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -from pandas import DataFrame +from pandas import DataFrame, NaT from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT @@ -159,7 +159,7 @@ class Backtesting: logger.info(f'Loading data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + f'({(max_date - min_date).days} days).') # Adjust startts forward if not enough data is available timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), @@ -449,10 +449,12 @@ class Backtesting: preprocessed[pair] = trim_dataframe(df, timerange, startup_candles=self.required_startup) min_date, max_date = history.get_timerange(preprocessed) - + if min_date is NaT or max_date is NaT: + raise OperationalException( + "No data left after adjusting for startup candles. ") logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' - f'({(max_date - min_date).days} days)..') + f'({(max_date - min_date).days} days).') # Execute backtest and store results results = self.backtest( processed=preprocessed, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5dfc9dbc3..2ebea564b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -366,7 +366,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: # check the logs, that will contain the backtest result exists = [ 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:59:00 (0 days)..' + 'up to 2017-11-14 22:59:00 (0 days).' ] for line in exists: assert log_has(line, caplog) @@ -791,9 +791,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...' ] @@ -864,9 +864,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', @@ -960,9 +960,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days)..', + 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', From 513be11fd98f86bc2eeda74ca42251f9b3521969 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 May 2021 20:23:11 +0200 Subject: [PATCH 327/460] Fix hyperopt output closes #4892 --- freqtrade/optimize/hyperopt_tools.py | 3 ++- tests/optimize/test_hyperopt.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index f655582c4..8d6098257 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -157,7 +157,8 @@ class HyperoptTools(): result = '{\n' for k, param in p.items(): - result += " " * indent + f'"{k}": {param},' + result += " " * indent + f'"{k}": ' + result += f'"{param}",' if isinstance(param, str) else f'{param},' if k in non_optimized: result += " # value loaded from strategy" result += "\n" diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 774dd35a4..16b647df9 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1156,3 +1156,17 @@ def test_SKDecimal(): assert space.transform([2.0]) == [200] assert space.transform([1.0]) == [100] assert space.transform([1.5, 1.6]) == [150, 160] + + +def test___pprint(): + params = {'buy_std': 1.2, 'buy_rsi': 31, 'buy_enable': True, 'buy_what': 'asdf'} + non_params = {'buy_notoptimied': 55} + + x = HyperoptTools._pprint(params, non_params) + assert x == """{ + "buy_std": 1.2, + "buy_rsi": 31, + "buy_enable": True, + "buy_what": "asdf", + "buy_notoptimied": 55, # value loaded from strategy +}""" From d34da3f981496068928d85f6378be6b10b6678b5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:17:59 +0300 Subject: [PATCH 328/460] Revert "Add dataframe parameter to custom_stoploss() and custom_sell() methods." This reverts commit 595b8735f80df633834a4d8266694cdcb52287b8. # Conflicts: # freqtrade/optimize/backtesting.py # freqtrade/strategy/interface.py --- freqtrade/freqtradebot.py | 13 ++++++------- freqtrade/optimize/backtesting.py | 7 +++---- freqtrade/strategy/interface.py | 21 +++++++++------------ tests/strategy/test_default_strategy.py | 3 +-- tests/strategy/test_interface.py | 4 ++-- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c2b15d23f..09a5ea746 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,7 +11,6 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from pandas import DataFrame from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -784,10 +783,10 @@ class FreqtradeBot(LoggingMixin): config_ask_strategy = self.config.get('ask_strategy', {}) - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) @@ -814,13 +813,13 @@ class FreqtradeBot(LoggingMixin): # resulting in outdated RPC messages self._sell_rate_cache[trade.pair] = sell_rate - if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -951,13 +950,13 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float, + def _check_and_execute_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: """ Check and execute sell """ should_sell = self.strategy.should_sell( - dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 899da03e4..80d816985 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -247,10 +247,9 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade, - sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) @@ -398,7 +397,7 @@ class Backtesting: for trade in open_trades[pair]: # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(processed[pair], trade, row) + trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: # logger.debug(f"{pair} - Backtesting sell {trade}") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 63dcc75d9..c483e6afb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,7 +277,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: DataFrame, **kwargs) -> float: + current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -300,8 +300,7 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: DataFrame, - **kwargs) -> Optional[Union[str, bool]]: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ Custom sell signal logic indicating that specified position should be sold. Returning a string or True from this method is equal to setting sell signal on a candle at specified @@ -539,8 +538,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, - buy: bool, sell: bool, low: float = None, high: float = None, + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell @@ -556,9 +555,8 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate, - trade=trade, current_time=date, - current_profit=current_profit, + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) # Set current rate to high for backtesting sell @@ -583,7 +581,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, - current_profit=current_profit, dataframe=dataframe) + current_profit=current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -620,7 +618,7 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, + def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ @@ -638,8 +636,7 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, - current_profit=current_profit, - dataframe=dataframe) + current_profit=current_profit) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index a8862e9c9..ec7b3c33d 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -41,5 +41,4 @@ def test_default_strategy(result, fee): rate=20000, time_in_force='gtc', sell_reason='roi') is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05, dataframe=None - ) == strategy.stoploss + current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a241d7f43..182dde335 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_time=now, current_profit=profit, - force_stoploss=0, high=None, dataframe=None) + force_stoploss=0, high=None) assert isinstance(sl_flag, SellCheckTuple) assert sl_flag.sell_type == expected if expected == SellType.NONE: @@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, current_time=now, current_profit=profit2, - force_stoploss=0, high=None, dataframe=None) + force_stoploss=0, high=None) assert sl_flag.sell_type == expected2 if expected2 == SellType.NONE: assert sl_flag.sell_flag is False From dc6e702fecc0c7480b9c5291e290362d8527247b Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:20:25 +0300 Subject: [PATCH 329/460] Pass current_time to confirm_trade_entry/confirm_trade_exit. --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/strategy/interface.py | 7 +++++-- tests/strategy/test_default_strategy.py | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 09a5ea746..d2e6ed417 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -552,7 +552,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, - time_in_force=time_in_force): + time_in_force=time_in_force, current_time=datetime.utcnow()): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -1190,8 +1190,8 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, - sell_reason=sell_reason.sell_reason): + time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, + current_time=datetime.utcnow()): logger.info(f"User requested abortion of selling {trade.pair}") return False diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c483e6afb..66adc36ec 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -229,7 +229,7 @@ class IStrategy(ABC, HyperStrategyMixin): pass def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, **kwargs) -> bool: + time_in_force: str, current_time: datetime, **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -244,6 +244,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param amount: Amount in target (quote) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process @@ -251,7 +252,8 @@ class IStrategy(ABC, HyperStrategyMixin): return True def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: + rate: float, time_in_force: str, sell_reason: str, + current_time: datetime, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or @@ -270,6 +272,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param sell_reason: Sell reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the sell-order is placed on the exchange. False aborts the process diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index ec7b3c33d..92ac9f63a 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -36,9 +36,11 @@ def test_default_strategy(result, fee): ) assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, - rate=20000, time_in_force='gtc') is True + rate=20000, time_in_force='gtc', + current_time=datetime.utcnow()) is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, - rate=20000, time_in_force='gtc', sell_reason='roi') is True + rate=20000, time_in_force='gtc', sell_reason='roi', + current_time=datetime.utcnow()) is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), current_rate=20_000, current_profit=0.05) == strategy.stoploss From cdfa6adbe55af690c7e7eb007a53f8a566ec0f84 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:20:54 +0300 Subject: [PATCH 330/460] Store pair datafrmes in dataprovider for backtesting. --- freqtrade/data/dataprovider.py | 3 +++ freqtrade/optimize/backtesting.py | 16 +++++++++++----- tests/strategy/test_interface.py | 8 ++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b4dea0743..6cc32157e 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -173,3 +173,6 @@ class DataProvider: return self._pairlists.whitelist.copy() else: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") + + def clear_cache(self): + self.__cached_pairs = {} diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 80d816985..73ad4e774 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -64,8 +64,8 @@ class Backtesting: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - dataprovider = DataProvider(self.config, self.exchange) - IStrategy.dp = dataprovider + self.dataprovider = DataProvider(self.config, self.exchange) + IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): @@ -96,7 +96,7 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) - dataprovider.add_pairlisthandler(self.pairlists) + self.dataprovider.add_pairlisthandler(self.pairlists) self.pairlists.refresh_pairlist() if len(self.pairlists.whitelist) == 0: @@ -176,6 +176,7 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + self.dataprovider.clear_cache() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ @@ -266,7 +267,8 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_reason): + sell_reason=sell.sell_reason, + current_time=sell_row[DATE_IDX].to_pydatetime()): return None trade.close(closerate, show_msg=False) @@ -286,7 +288,7 @@ class Backtesting: # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], - time_in_force=time_in_force): + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): @@ -348,6 +350,10 @@ class Backtesting: trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) + # Update dataprovider cache + for pair, dataframe in processed.items(): + self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) + # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) data: Dict = self._get_ohlcv_as_lists(processed) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 182dde335..ded396779 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -399,27 +399,27 @@ def test_custom_sell(default_conf, fee, caplog) -> None: ) now = arrow.utcnow().datetime - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_flag is False assert res.sell_type == SellType.NONE strategy.custom_sell = MagicMock(return_value=True) - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_flag is True assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_reason == 'custom_sell' strategy.custom_sell = MagicMock(return_value='hello world') - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'hello world' caplog.clear() strategy.custom_sell = MagicMock(return_value='h' * 100) - res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'h' * 64 From 6af4de8fe86f66746a312144a9964cbbcc5ebb43 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 12:25:43 +0300 Subject: [PATCH 331/460] Remove dataframe parameter from docs. --- docs/strategy-advanced.md | 22 +++++++++------------- docs/strategy-customization.md | 3 +-- freqtrade/strategy/interface.py | 1 - 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index eb322df9d..383b2a1a9 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -60,7 +60,8 @@ from freqtrade.strategy import IStrategy, timeframe_to_prev_date class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, dataframe: DataFrame, **kwargs): + current_profit: float, **kwargs): + dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe) trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() @@ -105,8 +106,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -156,8 +156,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -183,8 +182,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -210,8 +208,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -250,8 +247,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -293,8 +289,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # Default return value result = 1 @@ -302,6 +297,7 @@ class AwesomeStrategy(IStrategy): # Using current_time directly would only work in backtesting. Live/dry runs need time to # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. + dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe) current_time = timeframe_to_prev_date(self.timeframe, current_time) current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() if 'atr' in current_row: diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 59bfbde48..6c62c1e86 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -631,8 +631,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 66adc36ec..7483abf6d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -296,7 +296,6 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ From 6fb4d83ab3f2cb7b4402223b073ccb0a868fcb76 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 2 May 2021 16:35:06 +0300 Subject: [PATCH 332/460] Fix dataprovider in hyperopt. --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/hyperopt.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 73ad4e774..6d8b329bb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -353,6 +353,7 @@ class Backtesting: # Update dataprovider cache for pair, dataframe in processed.items(): self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) + self.strategy.dp = self.dataprovider # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e0a6d50a0..5e3d01047 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -372,8 +372,6 @@ class Hyperopt: self.backtesting.exchange._api_async = None # type: ignore # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore - self.backtesting.strategy.dp = None # type: ignore - IStrategy.dp = None # type: ignore cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") From 9b4f6b41a2ade4d1857fa0a6ed223967a965b26f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 3 May 2021 09:18:38 +0300 Subject: [PATCH 333/460] Use correct datetime. --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d2e6ed417..b3379a462 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -552,7 +552,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, - time_in_force=time_in_force, current_time=datetime.utcnow()): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -1191,7 +1191,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.utcnow()): + current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of selling {trade.pair}") return False From d344194b360bd453c4c6ea673a2724ef72b0e8ad Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 3 May 2021 09:47:58 +0300 Subject: [PATCH 334/460] Fix dataprovider in hyperopt. --- freqtrade/data/dataprovider.py | 141 +++++++++++++++++------------- freqtrade/optimize/backtesting.py | 4 +- 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 6cc32157e..731815572 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -45,40 +45,6 @@ class DataProvider: """ self._pairlists = pairlists - def refresh(self, - pairlist: ListPairsWithTimeframes, - helping_pairs: ListPairsWithTimeframes = None) -> None: - """ - Refresh data, called with each cycle - """ - if helping_pairs: - self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) - else: - self._exchange.refresh_latest_ohlcv(pairlist) - - @property - def available_pairs(self) -> ListPairsWithTimeframes: - """ - Return a list of tuples containing (pair, timeframe) for which data is currently cached. - Should be whitelist + open trades. - """ - return list(self._exchange._klines.keys()) - - def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: - """ - Get candle (OHLCV) data for the given pair as DataFrame - Please use the `available_pairs` method to verify which pairs are currently cached. - :param pair: pair to get the data for - :param timeframe: Timeframe to get data for - :param copy: copy dataframe before returning if True. - Use False only for read-only operations (where the dataframe is not modified) - """ - if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - return self._exchange.klines((pair, timeframe or self._config['timeframe']), - copy=copy) - else: - return DataFrame() - def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame: """ Get stored historical candle (OHLCV) data @@ -123,35 +89,6 @@ class DataProvider: return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) - def market(self, pair: str) -> Optional[Dict[str, Any]]: - """ - Return market data for the pair - :param pair: Pair to get the data for - :return: Market data dict from ccxt or None if market info is not available for the pair - """ - return self._exchange.markets.get(pair) - - def ticker(self, pair: str): - """ - Return last ticker data from exchange - :param pair: Pair to get the data for - :return: Ticker dict from exchange or empty dict if ticker is not available for the pair - """ - try: - return self._exchange.fetch_ticker(pair) - except ExchangeError: - return {} - - def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: - """ - Fetch latest l2 orderbook data - Warning: Does a network request - so use with common sense. - :param pair: pair to get the data for - :param maximum: Maximum number of orderbook entries to query - :return: dict including bids/asks with a total of `maximum` entries. - """ - return self._exchange.fetch_l2_order_book(pair, maximum) - @property def runmode(self) -> RunMode: """ @@ -175,4 +112,82 @@ class DataProvider: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") def clear_cache(self): + """ + Clear pair dataframe cache. + """ self.__cached_pairs = {} + + # Exchange functions + + def refresh(self, + pairlist: ListPairsWithTimeframes, + helping_pairs: ListPairsWithTimeframes = None) -> None: + """ + Refresh data, called with each cycle + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + if helping_pairs: + self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) + else: + self._exchange.refresh_latest_ohlcv(pairlist) + + @property + def available_pairs(self) -> ListPairsWithTimeframes: + """ + Return a list of tuples containing (pair, timeframe) for which data is currently cached. + Should be whitelist + open trades. + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + return list(self._exchange._klines.keys()) + + def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: + """ + Get candle (OHLCV) data for the given pair as DataFrame + Please use the `available_pairs` method to verify which pairs are currently cached. + :param pair: pair to get the data for + :param timeframe: Timeframe to get data for + :param copy: copy dataframe before returning if True. + Use False only for read-only operations (where the dataframe is not modified) + """ + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + return self._exchange.klines((pair, timeframe or self._config['timeframe']), + copy=copy) + else: + return DataFrame() + + def market(self, pair: str) -> Optional[Dict[str, Any]]: + """ + Return market data for the pair + :param pair: Pair to get the data for + :return: Market data dict from ccxt or None if market info is not available for the pair + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + return self._exchange.markets.get(pair) + + def ticker(self, pair: str): + """ + Return last ticker data from exchange + :param pair: Pair to get the data for + :return: Ticker dict from exchange or empty dict if ticker is not available for the pair + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + try: + return self._exchange.fetch_ticker(pair) + except ExchangeError: + return {} + + def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: + """ + Fetch latest l2 orderbook data + Warning: Does a network request - so use with common sense. + :param pair: pair to get the data for + :param maximum: Maximum number of orderbook entries to query + :return: dict including bids/asks with a total of `maximum` entries. + """ + if self._exchange is None: + raise OperationalException('Exchange is not available to DataProvider.') + return self._exchange.fetch_l2_order_book(pair, maximum) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6d8b329bb..d3e0afe7b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,9 +63,7 @@ class Backtesting: self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.dataprovider = DataProvider(self.config, self.exchange) - IStrategy.dp = self.dataprovider if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): @@ -132,6 +130,7 @@ class Backtesting: Load strategy into backtesting """ self.strategy: IStrategy = strategy + strategy.dp = self.dataprovider # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case @@ -353,7 +352,6 @@ class Backtesting: # Update dataprovider cache for pair, dataframe in processed.items(): self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) - self.strategy.dp = self.dataprovider # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) From 4b6cd69c81e1a3442941ec03c57f26260e57812d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 May 2021 20:08:31 +0200 Subject: [PATCH 335/460] Add test for no-exchange dataprovider --- freqtrade/data/dataprovider.py | 14 +++++++++----- freqtrade/optimize/hyperopt.py | 1 - tests/data/test_dataprovider.py | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 731815572..aad50e404 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -19,6 +19,8 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) +NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' + class DataProvider: @@ -126,7 +128,7 @@ class DataProvider: Refresh data, called with each cycle """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) if helping_pairs: self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) else: @@ -139,7 +141,7 @@ class DataProvider: Should be whitelist + open trades. """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) return list(self._exchange._klines.keys()) def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: @@ -151,6 +153,8 @@ class DataProvider: :param copy: copy dataframe before returning if True. Use False only for read-only operations (where the dataframe is not modified) """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): return self._exchange.klines((pair, timeframe or self._config['timeframe']), copy=copy) @@ -164,7 +168,7 @@ class DataProvider: :return: Market data dict from ccxt or None if market info is not available for the pair """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.markets.get(pair) def ticker(self, pair: str): @@ -174,7 +178,7 @@ class DataProvider: :return: Ticker dict from exchange or empty dict if ticker is not available for the pair """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) try: return self._exchange.fetch_ticker(pair) except ExchangeError: @@ -189,5 +193,5 @@ class DataProvider: :return: dict including bids/asks with a total of `maximum` entries. """ if self._exchange is None: - raise OperationalException('Exchange is not available to DataProvider.') + raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 5e3d01047..5ccf02d01 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -31,7 +31,6 @@ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver -from freqtrade.strategy import IStrategy # Suppress scikit-learn FutureWarnings from skopt diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 6b33fa7f2..c3b210d9d 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -246,3 +246,24 @@ def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history): assert dataframe.empty assert isinstance(time, datetime) assert time == datetime(1970, 1, 1, tzinfo=timezone.utc) + + +def test_no_exchange_mode(default_conf): + dp = DataProvider(default_conf, None) + + message = "Exchange is not available to DataProvider." + + with pytest.raises(OperationalException, match=message): + dp.refresh([()]) + + with pytest.raises(OperationalException, match=message): + dp.ohlcv('XRP/USDT', '5m') + + with pytest.raises(OperationalException, match=message): + dp.market('XRP/USDT') + + with pytest.raises(OperationalException, match=message): + dp.ticker('XRP/USDT') + + with pytest.raises(OperationalException, match=message): + dp.orderbook('XRP/USDT', 20) From 1b01ad6f8588d4bd145f05ec6137b30e60b6b352 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 7 May 2021 17:27:48 +0300 Subject: [PATCH 336/460] Make exchange parameter optional and do not use it as parameter in backtesting. --- freqtrade/data/dataprovider.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index aad50e404..727768ebb 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -24,7 +24,7 @@ NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' class DataProvider: - def __init__(self, config: dict, exchange: Exchange, pairlists=None) -> None: + def __init__(self, config: dict, exchange: Optional[Exchange], pairlists=None) -> None: self._config = config self._exchange = exchange self._pairlists = pairlists diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d3e0afe7b..150baa9bc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,7 +63,7 @@ class Backtesting: self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.dataprovider = DataProvider(self.config, self.exchange) + self.dataprovider = DataProvider(self.config, None) if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): From f1eb6535452fb1598b9999b59ae2e5167ce074f7 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 7 May 2021 17:28:06 +0300 Subject: [PATCH 337/460] Fix strategy protections not being loaded in backtesting. --- freqtrade/optimize/backtesting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 150baa9bc..80eb34c30 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -110,8 +110,6 @@ class Backtesting: PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False PairLocks.reset_locks() - if self.config.get('enable_protections', False): - self.protections = ProtectionManager(self.config) self.wallets = Wallets(self.config, self.exchange, log=False) @@ -135,6 +133,12 @@ class Backtesting: # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + if self.config.get('enable_protections', False): + conf = self.config + if hasattr(strategy, 'protections'): + conf = deepcopy(conf) + conf['protections'] = strategy.protections + self.protections = ProtectionManager(conf) def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: """ From 70189b19920bfaf705196fa3e0da72d89967ac43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 May 2021 17:24:41 +0200 Subject: [PATCH 338/460] Move dockerfile and document M1 image existance --- .../Dockerfile.aarch64 | 0 docs/docker_quickstart.md | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+) rename Dockerfile.aarch64 => docker/Dockerfile.aarch64 (100%) diff --git a/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 similarity index 100% rename from Dockerfile.aarch64 rename to docker/Dockerfile.aarch64 diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 5f48782d2..8d8582609 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -48,6 +48,8 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Download the docker-compose file from the repository curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml + # Edit the compose file to use an image named `*_pi` (stable_pi or develop_pi) + # Pull the freqtrade image docker-compose pull @@ -65,6 +67,30 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # image: freqtradeorg/freqtrade:develop_pi ``` +=== "ARM64 (Mac M1)" + Make sure that your docker installation is running in native mode + + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml + + # Edit the compose file, uncomment the "build" step and use "./docker/Dockerfile.aarch64" + # Also, change the image name to something of your liking + + # Build the freqtrade image (this may take a while) + docker-compose build + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + !!! Warning + You should not use the default image name - this can result in conflicting names between local and dockerhub and should therefore be avoided. + The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. From 8d8c782bd039780f8563e8edf7f735501c9013ab Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 8 May 2021 16:06:19 +0300 Subject: [PATCH 339/460] Slice dataframe in backtesting, preventing access to rows past current time. --- freqtrade/data/dataprovider.py | 21 ++++++++++++++++++--- freqtrade/optimize/backtesting.py | 8 ++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 727768ebb..a5d059e4a 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -20,6 +20,7 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' +MAX_DATAFRAME_CANDLES = 1000 class DataProvider: @@ -29,6 +30,14 @@ class DataProvider: self._exchange = exchange self._pairlists = pairlists self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} + self.__slice_index = None + + def _set_dataframe_max_index(self, limit_index: int): + """ + Limit analyzed dataframe to max specified index. + :param limit_index: dataframe index. + """ + self.__slice_index = limit_index def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -85,10 +94,16 @@ class DataProvider: combination. Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached. """ - if (pair, timeframe) in self.__cached_pairs: - return self.__cached_pairs[(pair, timeframe)] + pair_key = (pair, timeframe) + if pair_key in self.__cached_pairs: + if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + df, date = self.__cached_pairs[pair_key] + else: + max_index = self.__slice_index + df, date = self.__cached_pairs[pair_key] + df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index] + return df, date else: - return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) @property diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 80eb34c30..f7c984047 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -373,8 +373,9 @@ class Backtesting: open_trade_count_start = open_trade_count for i, pair in enumerate(data): + row_index = indexes[pair] try: - row = data[pair][indexes[pair]] + row = data[pair][row_index] except IndexError: # missing Data for one pair at the end. # Warnings for this are shown during data loading @@ -383,7 +384,10 @@ class Backtesting: # Waits until the time-counter reaches the start of the data for this pair. if row[DATE_IDX] > tmp: continue - indexes[pair] += 1 + + row_index += 1 + self.dataprovider._set_dataframe_max_index(row_index) # noqa + indexes[pair] = row_index # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected From 17b9e898d2a234b9c1861192af21618ca63442a1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 8 May 2021 16:56:59 +0300 Subject: [PATCH 340/460] Update docs displaying how to get last available and trade-open candles. --- docs/strategy-advanced.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 383b2a1a9..ceadf0ab0 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -265,12 +265,6 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -!!! Warning - Only use `dataframe` values up until and including `current_time` value. Reading past - `current_time` you will look into the future, which will produce incorrect backtesting results - and throw an exception in dry/live runs. - see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. - !!! Note `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and `trade.open_date_utc` will not match the candle date precisely and using them directly will throw @@ -290,7 +284,6 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # Default return value result = 1 if trade: @@ -298,11 +291,19 @@ class AwesomeStrategy(IStrategy): # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe) - current_time = timeframe_to_prev_date(self.timeframe, current_time) - current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() - if 'atr' in current_row: + current_candle = dataframe.loc[-1].squeeze() + if 'atr' in current_candle: # new stoploss relative to current_rate - new_stoploss = (current_rate - current_row['atr']) / current_rate + new_stoploss = (current_rate - current_candle['atr']) / current_rate + + # Round trade date to it's candle time. + trade_date = timeframe_to_prev_date(trade.open_date_utc) + trade_candle = dataframe.loc[dataframe['date'] == trade_date] + # Just opened trades do not have their candle complete yet therefore trade_candle may be None + if trade_candle is not None: + trade_candle = trade_candle.squeeze() + trade_stoploss = (current_rate - trade_candle['atr']) / current_rate + new_stoploss = max(new_stoploss, trade_stoploss) # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 From 2157923aeedb9c7e5d8ab386812843e99aa43e19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 8 May 2021 19:43:31 +0200 Subject: [PATCH 341/460] have edge send multiple messages if necessary closes #4519 --- freqtrade/edge/edge_positioning.py | 2 +- freqtrade/rpc/telegram.py | 14 +++++++++++--- tests/rpc/test_rpc_telegram.py | 9 +++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 721e22262..0449d6ebe 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -240,7 +240,7 @@ class Edge: return self._final_pairs - def accepted_pairs(self) -> list: + def accepted_pairs(self) -> List[Dict[str, Any]]: """ return a list of accepted pairs along with their winrate, expectancy and stoploss """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 97a01fc53..c619559de 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -861,9 +861,17 @@ class Telegram(RPCHandler): """ try: edge_pairs = self._rpc._rpc_edge() - edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') - message = f'Edge only validated following pairs:\n
{edge_pairs_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML) + if not edge_pairs: + message = 'Edge only validated following pairs:' + self._send_msg(message, parse_mode=ParseMode.HTML) + + for chunk in chunks(edge_pairs, 25): + edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') + message = (f'Edge only validated following pairs:\n' + f'
{edge_pairs_tab}
') + + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6a36c12a7..6d42a6845 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1102,6 +1102,15 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
 
+    msg_mock.reset_mock()
+
+    mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
+        return_value={}))
+    telegram._edge(update=update, context=MagicMock())
+    assert msg_mock.call_count == 1
+    assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
+    assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
+
 
 def test_telegram_trades(mocker, update, default_conf, fee):
 

From 92186d89a2fcbbb03eaf789ccf52238b9b934936 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 09:56:36 +0200
Subject: [PATCH 342/460] Add some changes to strategytemplate

---
 freqtrade/data/dataprovider.py                           | 9 ++++++---
 freqtrade/optimize/backtesting.py                        | 2 +-
 .../templates/subtemplates/strategy_methods_advanced.j2  | 7 +++++--
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py
index a5d059e4a..1a86eece5 100644
--- a/freqtrade/data/dataprovider.py
+++ b/freqtrade/data/dataprovider.py
@@ -30,7 +30,7 @@ class DataProvider:
         self._exchange = exchange
         self._pairlists = pairlists
         self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
-        self.__slice_index = None
+        self.__slice_index: Optional[int] = None
 
     def _set_dataframe_max_index(self, limit_index: int):
         """
@@ -88,6 +88,8 @@ class DataProvider:
 
     def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
         """
+        Retrieve the analyzed dataframe. Returns the full dataframe in trade mode (live / dry),
+        and the last 1000 candles (up to the time evaluated at this moment) in all other modes.
         :param pair: pair to get the data for
         :param timeframe: timeframe to get data for
         :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe
@@ -99,9 +101,10 @@ class DataProvider:
             if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
                 df, date = self.__cached_pairs[pair_key]
             else:
-                max_index = self.__slice_index
                 df, date = self.__cached_pairs[pair_key]
-                df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index]
+                if self.__slice_index is not None:
+                    max_index = self.__slice_index
+                    df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index]
             return df, date
         else:
             return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index f7c984047..e057d8189 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -386,7 +386,7 @@ class Backtesting:
                     continue
 
                 row_index += 1
-                self.dataprovider._set_dataframe_max_index(row_index)   # noqa
+                self.dataprovider._set_dataframe_max_index(row_index)
                 indexes[pair] = row_index
 
                 # without positionstacking, we can only have one open trade per pair.
diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
index c69b52cad..2a9ac0690 100644
--- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
+++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
@@ -39,7 +39,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
     return self.stoploss
 
 def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
-                        time_in_force: str, **kwargs) -> bool:
+                        time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
     """
     Called right before placing a buy order.
     Timing for this function is critical, so avoid doing heavy computations or
@@ -54,6 +54,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
     :param amount: Amount in target (quote) currency that's going to be traded.
     :param rate: Rate that's going to be used when using limit orders
     :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
+    :param current_time: datetime object, containing the current datetime
     :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
     :return bool: When True is returned, then the buy-order is placed on the exchange.
         False aborts the process
@@ -61,7 +62,8 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
     return True
 
 def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
-                       rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
+                       rate: float, time_in_force: str, sell_reason: str,
+                       current_time: 'datetime', **kwargs) -> bool:
     """
     Called right before placing a regular sell order.
     Timing for this function is critical, so avoid doing heavy computations or
@@ -80,6 +82,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
     :param sell_reason: Sell reason.
         Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
                         'sell_signal', 'force_sell', 'emergency_sell']
+    :param current_time: datetime object, containing the current datetime
     :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
     :return bool: When True is returned, then the sell-order is placed on the exchange.
         False aborts the process

From 00e93dad024c2e1be045d4d66cbba3d52928e081 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 10:04:56 +0200
Subject: [PATCH 343/460] Fix mistake in the docs

---
 docs/strategy-advanced.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index ceadf0ab0..e52beec3b 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -61,7 +61,7 @@ from freqtrade.strategy import IStrategy, timeframe_to_prev_date
 class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):
-        dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
         trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
         trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
 
@@ -290,7 +290,7 @@ class AwesomeStrategy(IStrategy):
             # Using current_time directly would only work in backtesting. Live/dry runs need time to
             # be rounded to previous candle to be used as dataframe index. Rounding must also be 
             # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
-            dataframe = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
             current_candle = dataframe.loc[-1].squeeze()
             if 'atr' in current_candle:
                 # new stoploss relative to current_rate

From 0a0e7ce5f502cdf1e9797daed7d162167075a2c2 Mon Sep 17 00:00:00 2001
From: Boris Pruessmann 
Date: Sun, 9 May 2021 14:28:54 +0200
Subject: [PATCH 344/460] Documentation for running arm64 builds

---
 docs/docker_quickstart.md | 24 ++++++++++++++----------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index 8d8582609..ce6d0b503 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -67,29 +67,33 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
         # image: freqtradeorg/freqtrade:develop_pi
         ```
 
-=== "ARM64 (Mac M1)"
-    Make sure that your docker installation is running in native mode
+=== "ARM 64 Systenms (Jetson Nano, Mac M1, Raspberry Pi 4 8GB)"
+    In case of a Mac M1, make sure that your docker installation is running in native mode
 
     ``` bash
     mkdir ft_userdata
     cd ft_userdata/
+
+    # arm64 images are not yer provided via Docker Hub and need to be build locally first. Depending on the device,
+    # this may take a few minutes (Apple M1) or up to two hours (Raspberry Pi)
+    git clone  https://github.com/freqtrade/freqtrade.git
+    docker build -f ./freqtrade/docker/Dockerfile.aarch64 -t freqtradeorg/freqtrade:develop_arm64 freqtrade
+
     # Download the docker-compose file from the repository
     curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
 
-    # Edit the compose file, uncomment the "build" step and use "./docker/Dockerfile.aarch64"
-    # Also, change the image name to something of your liking
-
-    # Build the freqtrade image (this may take a while)
-    docker-compose build
-
     # Create user directory structure
     docker-compose run --rm freqtrade create-userdir --userdir user_data
 
     # Create configuration - Requires answering interactive questions
     docker-compose run --rm freqtrade new-config --config user_data/config.json
     ```
-    !!! Warning
-        You should not use the default image name - this can result in conflicting names between local and dockerhub and should therefore be avoided.
+
+    !!! Note "Change your docker Image"
+        You have to change the docker image in the docker-compose file for your arm64 build to work properly.
+        ``` yml
+        image: freqtradeorg/freqtrade:develop_arm64
+        ```
 
 The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
 The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections.

From f2add44253f62fe67f10d5cb3c30dd10b649f82c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?=
 
Date: Sun, 9 May 2021 17:27:30 +0200
Subject: [PATCH 345/460] Update docs/docker_quickstart.md

Co-authored-by: Matthias 
---
 docs/docker_quickstart.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index ce6d0b503..51386a4e3 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -74,7 +74,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
     mkdir ft_userdata
     cd ft_userdata/
 
-    # arm64 images are not yer provided via Docker Hub and need to be build locally first. Depending on the device,
+    # arm64 images are not yet provided via Docker Hub and need to be build locally first. Depending on the device,
     # this may take a few minutes (Apple M1) or up to two hours (Raspberry Pi)
     git clone  https://github.com/freqtrade/freqtrade.git
     docker build -f ./freqtrade/docker/Dockerfile.aarch64 -t freqtradeorg/freqtrade:develop_arm64 freqtrade

From 1c408c04041830cac86969bec0d026afa9717870 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 19:47:37 +0200
Subject: [PATCH 346/460] Add small tests for backtest mode

---
 tests/data/test_dataprovider.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py
index c3b210d9d..b87258c64 100644
--- a/tests/data/test_dataprovider.py
+++ b/tests/data/test_dataprovider.py
@@ -247,6 +247,25 @@ def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history):
     assert isinstance(time, datetime)
     assert time == datetime(1970, 1, 1, tzinfo=timezone.utc)
 
+    # Test backtest mode
+    default_conf["runmode"] = RunMode.BACKTEST
+    dp._set_dataframe_max_index(1)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+
+    assert len(dataframe) == 1
+
+    dp._set_dataframe_max_index(2)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+    assert len(dataframe) == 2
+
+    dp._set_dataframe_max_index(3)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+    assert len(dataframe) == 3
+
+    dp._set_dataframe_max_index(500)
+    dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
+    assert len(dataframe) == len(ohlcv_history)
+
 
 def test_no_exchange_mode(default_conf):
     dp = DataProvider(default_conf, None)
@@ -267,3 +286,6 @@ def test_no_exchange_mode(default_conf):
 
     with pytest.raises(OperationalException, match=message):
         dp.orderbook('XRP/USDT', 20)
+
+    with pytest.raises(OperationalException, match=message):
+        dp.available_pairs()

From d495ea36932011babb3dcafeaa1795a5e986f71c Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 9 May 2021 19:53:41 +0200
Subject: [PATCH 347/460] Update docs about availability of get_analyzed

---
 docs/strategy-customization.md | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md
index 6c62c1e86..49456c047 100644
--- a/docs/strategy-customization.md
+++ b/docs/strategy-customization.md
@@ -422,10 +422,6 @@ if self.dp:
     Returns an empty dataframe if the requested pair was not cached.
     This should not happen when using whitelisted pairs.
 
-
-!!! Warning "Warning about backtesting"
-    This method will return an empty dataframe during backtesting.
-
 ### *orderbook(pair, maximum)*
 
 ``` python

From a7bd051f6b2aac583a7448d8fe514fea817eb3b2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:26:26 +0000
Subject: [PATCH 348/460] Bump mkdocs-material from 7.1.3 to 7.1.4

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.3 to 7.1.4.
- [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/7.1.3...7.1.4)

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 a431b1702..88f336cb1 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,3 +1,3 @@
-mkdocs-material==7.1.3
+mkdocs-material==7.1.4
 mdx_truly_sane_lists==1.2
 pymdown-extensions==8.1.1

From a7cd8fc578372cae26fc257c6cfcd10998d84eb4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:26:36 +0000
Subject: [PATCH 349/460] Bump sqlalchemy from 1.4.12 to 1.4.14

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.12 to 1.4.14.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 49e1758df..f0676fdea 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@ ccxt==1.49.30
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0
-SQLAlchemy==1.4.12
+SQLAlchemy==1.4.14
 python-telegram-bot==13.5
 arrow==1.1.0
 cachetools==4.2.2

From 0a82e2b06143d37fde8ba002800f0d7c9a16907b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:26:52 +0000
Subject: [PATCH 350/460] Bump pytest from 6.2.3 to 6.2.4

Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.3...6.2.4)

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 adf5a81c3..2b2314f85 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -8,7 +8,7 @@ flake8==3.9.1
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==4.2.1
 mypy==0.812
-pytest==6.2.3
+pytest==6.2.4
 pytest-asyncio==0.15.1
 pytest-cov==2.11.1
 pytest-mock==3.6.0

From 6eb47b0aebf786ed533b991d56a586f135ffc3da Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:03 +0000
Subject: [PATCH 351/460] Bump flake8 from 3.9.1 to 3.9.2

Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.1 to 3.9.2.
- [Release notes](https://gitlab.com/pycqa/flake8/tags)
- [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.1...3.9.2)

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 adf5a81c3..ca297371f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,7 @@
 -r requirements-hyperopt.txt
 
 coveralls==3.0.1
-flake8==3.9.1
+flake8==3.9.2
 flake8-type-annotations==0.1.0
 flake8-tidy-imports==4.2.1
 mypy==0.812

From 5ecd86ed5694721fedddd93fd923311593b302e5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:11 +0000
Subject: [PATCH 352/460] Bump pymdown-extensions from 8.1.1 to 8.2

Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.1.1 to 8.2.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.1.1...8.2)

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 a431b1702..70b33287e 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,3 +1,3 @@
 mkdocs-material==7.1.3
 mdx_truly_sane_lists==1.2
-pymdown-extensions==8.1.1
+pymdown-extensions==8.2

From 93268ba16d5aba78518ca6ce2f21227f4e6c7a63 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:22 +0000
Subject: [PATCH 353/460] Bump pytest-mock from 3.6.0 to 3.6.1

Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.0...v3.6.1)

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index adf5a81c3..dfb28dc05 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -11,7 +11,7 @@ mypy==0.812
 pytest==6.2.3
 pytest-asyncio==0.15.1
 pytest-cov==2.11.1
-pytest-mock==3.6.0
+pytest-mock==3.6.1
 pytest-random-order==1.0.4
 isort==5.8.0
 

From 43c7382d24aba1381dde7776fc67f724f61316a5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 05:27:36 +0000
Subject: [PATCH 354/460] Bump fastapi from 0.63.0 to 0.64.0

Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.63.0 to 0.64.0.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.63.0...0.64.0)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 49e1758df..7d72a63da 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -31,7 +31,7 @@ python-rapidjson==1.0
 sdnotify==0.3.2
 
 # API Server
-fastapi==0.63.0
+fastapi==0.64.0
 uvicorn==0.13.4
 pyjwt==2.1.0
 aiofiles==0.6.0

From 8e6a95e11b57c4060955bd4091972de40e766a50 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 May 2021 10:50:05 +0000
Subject: [PATCH 355/460] Bump ccxt from 1.49.30 to 1.49.73

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.49.30 to 1.49.73.
- [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.49.30...1.49.73)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 281bf4574..3c50fa586 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 numpy==1.20.2
 pandas==1.2.4
 
-ccxt==1.49.30
+ccxt==1.49.73
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0

From 3d6b3f1d6aa39fe46b8d684d8be76d8986866293 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Mon, 10 May 2021 15:08:28 +0200
Subject: [PATCH 356/460] Add Issue config.yml

---
 .github/ISSUE_TEMPLATE/config.yml | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 .github/ISSUE_TEMPLATE/config.yml

diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..7591c1fb0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,6 @@
+---
+blank_issues_enabled: false
+contact_links:
+  - name: Discord Server
+    url: https://discord.gg/MA9v74M
+    about: Ask a question or get community support from our Discord server

From 425d97719abbf04747508e1c7d1f4e8b62578451 Mon Sep 17 00:00:00 2001
From: Robert Davey 
Date: Mon, 10 May 2021 19:42:37 +0100
Subject: [PATCH 357/460] Update strategy-advanced.md

Update custom_sell() example to comment that the current trade row is at trade open as written. Change "abstain" to something clearer for non-fluent English speakers.
---
 docs/strategy-advanced.md | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index e52beec3b..7285b24af 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -48,7 +48,7 @@ It is possible to define custom sell signals, indicating that specified position
 
 For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
 
-You should abstain from using custom_sell() signals in place of stoplosses though. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
+Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
 
 !!! Note
     Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
@@ -62,17 +62,19 @@ class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):
         dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+        
+        # Get the row at trade open
         trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
-        trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
+        trade_open_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
 
         # Above 20% profit, sell when rsi < 80
         if current_profit > 0.2:
-            if trade_row['rsi'] < 80:
+            if trade_open_row['rsi'] < 80:
                 return 'rsi_below_80'
 
         # Between 2% and 10%, sell if EMA-long above EMA-short
         if 0.02 < current_profit < 0.1:
-            if trade_row['emalong'] > trade_row['emashort']:
+            if trade_open_row['emalong'] > trade_open_row['emashort']:
                 return 'ema_long_below_80'
 
         # Sell any positions at a loss if they are held for more than one day.

From bcab44560ab3b49edade5cb5ed7c9fcc72e47fc4 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 11 May 2021 06:23:21 +0200
Subject: [PATCH 358/460] Fix doc typo

---
 docs/strategy-advanced.md | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index 7285b24af..ada7a99be 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -293,7 +293,7 @@ class AwesomeStrategy(IStrategy):
             # be rounded to previous candle to be used as dataframe index. Rounding must also be 
             # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
             dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
-            current_candle = dataframe.loc[-1].squeeze()
+            current_candle = dataframe.iloc[-1].squeeze()
             if 'atr' in current_candle:
                 # new stoploss relative to current_rate
                 new_stoploss = (current_rate - current_candle['atr']) / current_rate
@@ -312,6 +312,11 @@ class AwesomeStrategy(IStrategy):
         return result
 ```
 
+!!! Warning "Using .iloc[-1]"
+    You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
+    This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
+    Also, this will only work starting with version 2021.5.
+
 ---
 
 ## Custom order timeout rules

From e53bbec285a9056e09942bec92dc719ba98ec99e Mon Sep 17 00:00:00 2001
From: Kamontat Chantrachirathumrong 
Date: Wed, 12 May 2021 00:13:13 +0700
Subject: [PATCH 359/460] remove duplicate python3-pip

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

diff --git a/docs/installation.md b/docs/installation.md
index d2661f88f..18f5f4d68 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
     sudo apt-get update
 
     # install packages
-    sudo apt install -y python3-pip python3-venv python3-pandas python3-pip git
+    sudo apt install -y python3-pip python3-venv python3-pandas git
     ```
 
 === "RaspberryPi/Raspbian"

From 7398ea88e0008787add66c83978040c6c1fa89af Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 11 May 2021 20:30:37 +0200
Subject: [PATCH 360/460] Change optimize_reports to convert dates to string
 earlier

---
 freqtrade/optimize/optimize_reports.py  | 16 ++++++++--------
 tests/optimize/test_optimize_reports.py | 10 +++++-----
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py
index 170e19ecc..2be3c3e62 100644
--- a/freqtrade/optimize/optimize_reports.py
+++ b/freqtrade/optimize/optimize_reports.py
@@ -313,9 +313,9 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
         'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
         'profit_total': results['profit_abs'].sum() / starting_balance,
         'profit_total_abs': results['profit_abs'].sum(),
-        'backtest_start': min_date,
+        'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
         'backtest_start_ts': int(min_date.timestamp() * 1000),
-        'backtest_end': max_date,
+        'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
         'backtest_end_ts': int(max_date.timestamp() * 1000),
         'backtest_days': backtest_days,
 
@@ -362,9 +362,9 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
         strat_stats.update({
             'max_drawdown': max_drawdown,
             'max_drawdown_abs': drawdown_abs,
-            'drawdown_start': drawdown_start,
+            'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
             'drawdown_start_ts': drawdown_start.timestamp() * 1000,
-            'drawdown_end': drawdown_end,
+            'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT),
             'drawdown_end_ts': drawdown_end.timestamp() * 1000,
 
             'max_drawdown_low': low_val,
@@ -497,8 +497,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
         best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
         worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
         metrics = [
-            ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
-            ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
+            ('Backtesting from', strat_results['backtest_start']),
+            ('Backtesting to', strat_results['backtest_end']),
             ('Max open trades', strat_results['max_open_trades']),
             ('', ''),  # Empty line to improve readability
             ('Total trades', strat_results['total_trades']),
@@ -546,8 +546,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
                                                strat_results['stake_currency'])),
             ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
                                               strat_results['stake_currency'])),
-            ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
-            ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
+            ('Drawdown Start', strat_results['drawdown_start']),
+            ('Drawdown End', strat_results['drawdown_end']),
             ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
         ]
 
diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py
index fb4624ca3..48ba416c0 100644
--- a/tests/optimize/test_optimize_reports.py
+++ b/tests/optimize/test_optimize_reports.py
@@ -7,7 +7,7 @@ import pytest
 from arrow import Arrow
 
 from freqtrade.configuration import TimeRange
-from freqtrade.constants import LAST_BT_RESULT_FN
+from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
 from freqtrade.data import history
 from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data
 from freqtrade.edge import PairInfo
@@ -97,8 +97,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
     assert 'DefStrat' in stats['strategy']
     assert 'strategy_comparison' in stats
     strat_stats = stats['strategy']['DefStrat']
-    assert strat_stats['backtest_start'] == min_date.datetime
-    assert strat_stats['backtest_end'] == max_date.datetime
+    assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT)
+    assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT)
     assert strat_stats['total_trades'] == len(results['DefStrat']['results'])
     # Above sample had no loosing trade
     assert strat_stats['max_drawdown'] == 0.0
@@ -141,8 +141,8 @@ def test_generate_backtest_stats(default_conf, testdatadir):
     strat_stats = stats['strategy']['DefStrat']
 
     assert strat_stats['max_drawdown'] == 0.013803
-    assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc)
-    assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc)
+    assert strat_stats['drawdown_start'] == '2017-11-14 22:10:00'
+    assert strat_stats['drawdown_end'] == '2017-11-14 22:43:00'
     assert strat_stats['drawdown_end_ts'] == 1510699380000
     assert strat_stats['drawdown_start_ts'] == 1510697400000
     assert strat_stats['pairlist'] == ['UNITTEST/BTC']

From 06bf1aa274be48f5c1d847349c34aa62fa1cdc67 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 05:58:25 +0200
Subject: [PATCH 361/460] Store epochs as json per line

---
 freqtrade/optimize/hyperopt.py  | 50 ++++++++++++++--------------
 tests/optimize/test_hyperopt.py | 59 ++++++++++++++++++---------------
 2 files changed, 57 insertions(+), 52 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 5ccf02d01..c14b62b9d 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -5,15 +5,16 @@ This module contains the hyperopt logic
 """
 
 import logging
+import os
 import random
 import warnings
 from datetime import datetime, timezone
 from math import ceil
-from operator import itemgetter
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
 import progressbar
+import rapidjson
 from colorama import Fore, Style
 from colorama import init as colorama_init
 from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
@@ -86,7 +87,7 @@ class Hyperopt:
         time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
         strategy = str(self.config['strategy'])
         self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' /
-                                   f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
+                                   f'strategy_{strategy}_hyperopt_results_{time_now}.fthypt')
         self.data_pickle_file = (self.config['user_data_dir'] /
                                  'hyperopt_results' / 'hyperopt_tickerdata.pkl')
         self.total_epochs = config.get('epochs', 0)
@@ -96,9 +97,7 @@ class Hyperopt:
         self.clean_hyperopt()
 
         self.num_epochs_saved = 0
-
-        # Previous evaluations
-        self.epochs: List = []
+        self.current_best_epoch: Optional[Dict[str, Any]] = None
 
         # Populate functions here (hasattr is slow so should not be run during "regular" operations)
         if hasattr(self.custom_hyperopt, 'populate_indicators'):
@@ -156,21 +155,24 @@ class Hyperopt:
         # and the values are taken from the list of parameters.
         return {d.name: v for d, v in zip(dimensions, raw_params)}
 
-    def _save_results(self) -> None:
+    def _save_result(self, epoch: Dict) -> None:
         """
         Save hyperopt results to file
+        Store one line per epoch.
+        While not a valid json object - this allows appending easily.
+        :param epoch: result dictionary for this epoch.
         """
-        num_epochs = len(self.epochs)
-        if num_epochs > self.num_epochs_saved:
-            logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.")
-            dump(self.epochs, self.results_file)
-            self.num_epochs_saved = num_epochs
-            logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
-                         f"saved to '{self.results_file}'.")
-            # Store hyperopt filename
-            latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
-            file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
-                           log=False)
+        with self.results_file.open('a') as f:
+            rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE)
+            f.write(os.linesep)
+
+        self.num_epochs_saved += 1
+        logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
+                     f"saved to '{self.results_file}'.")
+        # Store hyperopt filename
+        latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
+        file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
+                       log=False)
 
     def _get_params_details(self, params: Dict) -> Dict:
         """
@@ -442,25 +444,21 @@ class Hyperopt:
 
                             if is_best:
                                 self.current_best_loss = val['loss']
-                            self.epochs.append(val)
+                                self.current_best_epoch = val
 
-                            # Save results after each best epoch and every 100 epochs
-                            if is_best or current % 100 == 0:
-                                self._save_results()
+                            self._save_result(val)
 
                             pbar.update(current)
 
         except KeyboardInterrupt:
             print('User interrupted..')
 
-        self._save_results()
         logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
                     f"saved to '{self.results_file}'.")
 
-        if self.epochs:
-            sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
-            best_epoch = sorted_epochs[0]
-            HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
+        if self.current_best_epoch:
+            HyperoptTools.print_epoch_details(self.current_best_epoch, self.total_epochs,
+                                              self.print_json)
         else:
             # This is printed when Ctrl+C is pressed quickly, before first epochs have
             # a chance to be evaluated.
diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index 16b647df9..f9a9dba85 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -386,7 +386,8 @@ def test_roi_table_generation(hyperopt) -> None:
 
 
 def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
 
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -425,9 +426,9 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
 
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    # Should be called for historical candle data
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")
@@ -714,7 +715,8 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog):
 
 
 def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
 
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -765,13 +767,14 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
         ':{},"stoploss":null,"trailing_stop":null}'
     )
     assert result_str in out  # noqa: E501
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    # Should be called for historical candle data
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
 
 
 def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -813,13 +816,14 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
 
     out, err = capsys.readouterr()
     assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out  # noqa: E501
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    # Should be called for historical candle data
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
 
 
 def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -860,13 +864,14 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
 
     out, err = capsys.readouterr()
     assert '{"minimal_roi":{},"stoploss":null}' in out
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
 
 
 def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -908,9 +913,9 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
 
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
-    assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
+
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")
@@ -946,7 +951,8 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
 
 
 def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -989,8 +995,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
     assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")
@@ -999,7 +1005,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
 
 
 def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
-    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
+    dumper = mocker.patch('freqtrade.optimize.hyperopt.dump')
+    dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result')
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                  MagicMock(return_value=(MagicMock(), None)))
@@ -1042,8 +1049,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
     out, err = capsys.readouterr()
     assert 'Best result:\n\n*    1/1: foo result Objective: 1.00000\n' in out
     assert dumper.called
-    # Should be called twice, once for historical candle data, once to save evaluations
-    assert dumper.call_count == 2
+    assert dumper.call_count == 1
+    assert dumper2.call_count == 1
     assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
     assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
     assert hasattr(hyperopt, "max_open_trades")

From 3cbe40875de2e984bd01e42ae8905fe5e7b5a7e5 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 06:06:30 +0200
Subject: [PATCH 362/460] read hyperopt results from pickle or json

---
 freqtrade/optimize/hyperopt_tools.py | 25 ++++++++++++++++++++-----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
index 8d6098257..665c393b3 100644
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -31,15 +31,27 @@ class HyperoptTools():
         else:
             return any(s in config['spaces'] for s in [space, 'all', 'default'])
 
+    @staticmethod
+    def _read_results_pickle(results_file: Path) -> List:
+        """
+        Read hyperopt results from pickle file
+        LEGACY method - new files are written as json and cannot be read with this method.
+        """
+        from joblib import load
+
+        logger.info(f"Reading pickled epochs from '{results_file}'")
+        data = load(results_file)
+        return data
+
     @staticmethod
     def _read_results(results_file: Path) -> List:
         """
         Read hyperopt results from file
         """
-        from joblib import load
-
-        logger.info("Reading epochs from '%s'", results_file)
-        data = load(results_file)
+        import rapidjson
+        logger.info(f"Reading epochs from '{results_file}'")
+        with results_file.open('r') as f:
+            data = [rapidjson.loads(line) for line in f]
         return data
 
     @staticmethod
@@ -49,7 +61,10 @@ class HyperoptTools():
         """
         epochs: List = []
         if results_file.is_file() and results_file.stat().st_size > 0:
-            epochs = HyperoptTools._read_results(results_file)
+            if results_file.suffix == '.pickle':
+                epochs = HyperoptTools._read_results_pickle(results_file)
+            else:
+                epochs = HyperoptTools._read_results(results_file)
             # Detection of some old format, without 'is_best' field saved
             if epochs[0].get('is_best') is None:
                 raise OperationalException(

From ad4c51b3c5a36a089699931a05c566407a874bed Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Wed, 12 May 2021 09:30:35 +0300
Subject: [PATCH 363/460] * Added "Dataframe access" section showcasing how to
 obtain dataframe and use it to get last-available and trade-open candles. *
 Fix custom_sell() example to use rsi from last-available instead of
 trade-open candle, add a pointer to "Dataframe access" section for more info.
 * Simplify "Custom stoploss using an indicator from dataframe example"
 greatly, add a pointer to "Dataframe access" section for more info.

---
 docs/strategy-advanced.md | 100 +++++++++++++++++++++-----------------
 1 file changed, 55 insertions(+), 45 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index ada7a99be..0fd2b3c41 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -40,6 +40,41 @@ class AwesomeStrategy(IStrategy):
 !!! Note
     If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
 
+
+## Dataframe access
+
+You may access dataframe in various strategy functions by querying it from dataprovider.
+
+``` python
+from freqtrade.exchange import timeframe_to_prev_date
+
+class AwesomeStrategy(IStrategy):
+    def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
+                           rate: float, time_in_force: str, sell_reason: str,
+                           current_time: 'datetime', **kwargs) -> bool:
+        # Obtain pair dataframe.
+        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+
+        # Obtain last available candle. Do not use current_time to look up latest candle, because 
+        # current_time points to curret incomplete candle whose data is not available.
+        last_candle = dataframe.iloc[-1].squeeze()
+        # <...>
+
+        # In dry/live runs trade open date will not match candle open date therefore it must be 
+        # rounded.
+        trade_date = timeframe_to_prev_date(trade.open_date_utc)
+        # Look up trade candle.
+        trade_candle = dataframe.loc[dataframe['date'] == trade_date]
+        # trade_candle may be None for trades that just opened as it is stil lincomplete.
+        if trade_candle is not None:
+            # <...>
+```
+
+!!! Warning "Using .iloc[-1]"
+    You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
+    This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
+    Also, this will only work starting with version 2021.5.
+
 ***
 
 ## Custom sell signal
@@ -62,19 +97,16 @@ class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):
         dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
-        
-        # Get the row at trade open
-        trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
-        trade_open_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
+        last_candle = dataframe.iloc[-1].squeeze()
 
         # Above 20% profit, sell when rsi < 80
         if current_profit > 0.2:
-            if trade_open_row['rsi'] < 80:
+            if last_candle['rsi'] < 80:
                 return 'rsi_below_80'
 
         # Between 2% and 10%, sell if EMA-long above EMA-short
         if 0.02 < current_profit < 0.1:
-            if trade_open_row['emalong'] > trade_open_row['emashort']:
+            if last_candle['emalong'] > last_candle['emashort']:
                 return 'ema_long_below_80'
 
         # Sell any positions at a loss if they are held for more than one day.
@@ -82,7 +114,7 @@ class AwesomeStrategy(IStrategy):
             return 'unclog'
 ```
 
-See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter.
+See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
 
 ## Custom stoploss
 
@@ -265,57 +297,35 @@ class AwesomeStrategy(IStrategy):
 
 #### Custom stoploss using an indicator from dataframe example
 
-Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
-
-!!! Note
-    `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and
-    `trade.open_date_utc` will not match the candle date precisely and using them directly will throw
-    an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date
-    before using it to access `dataframe`.
+Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
 
 ``` python
-from freqtrade.exchange import timeframe_to_prev_date
-from freqtrade.persistence import Trade
-from freqtrade.state import RunMode
-
 class AwesomeStrategy(IStrategy):
 
-    # ... populate_* methods
+    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
+        # <...>
+        dataframe['sar'] = ta.SAR(dataframe)
 
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
                         current_rate: float, current_profit: float, **kwargs) -> float:
-        # Default return value
-        result = 1
-        if trade:
-            # Using current_time directly would only work in backtesting. Live/dry runs need time to
-            # be rounded to previous candle to be used as dataframe index. Rounding must also be 
-            # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
-            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
-            current_candle = dataframe.iloc[-1].squeeze()
-            if 'atr' in current_candle:
-                # new stoploss relative to current_rate
-                new_stoploss = (current_rate - current_candle['atr']) / current_rate
 
-                # Round trade date to it's candle time.
-                trade_date = timeframe_to_prev_date(trade.open_date_utc)
-                trade_candle = dataframe.loc[dataframe['date'] == trade_date]
-                # Just opened trades do not have their candle complete yet therefore trade_candle may be None
-                if trade_candle is not None:
-                    trade_candle = trade_candle.squeeze()
-                    trade_stoploss = (current_rate - trade_candle['atr']) / current_rate
-                    new_stoploss = max(new_stoploss, trade_stoploss)
-                # turn into relative negative offset required by `custom_stoploss` return implementation
-                result = new_stoploss - 1
+        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
+        last_candle = dataframe.iloc[-1].squeeze()
 
-        return result
+        # Use parabolic sar as absolute stoploss price
+        stoploss_price = last_candle['sar']
+
+        # Convert absolute price to percentage relative to current_rate
+        if stoploss_price < current_rate:
+            return (stoploss_price / current_rate) - 1
+
+        # return maximum stoploss value, keeping current stoploss price unchanged
+        return 1
 ```
 
-!!! Warning "Using .iloc[-1]"
-    You can use `.iloc[-1]` here because `get_analyzed_dataframe()` only returns candles that backtesting is allowed to see.
-    This will not work in `populate_*` methods, so make sure to not use `.iloc[]` in that area.
-    Also, this will only work starting with version 2021.5.
+See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
 
 ---
 

From 9bb6ba086bdeffe3b676cb0e0bf0e720b514fe8e Mon Sep 17 00:00:00 2001
From: Rokas Kupstys <19151258+rokups@users.noreply.github.com>
Date: Wed, 12 May 2021 17:15:38 +0300
Subject: [PATCH 364/460] Update docs/strategy-advanced.md

Co-authored-by: Matthias 
---
 docs/strategy-advanced.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index 0fd2b3c41..d533d81cd 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -65,7 +65,7 @@ class AwesomeStrategy(IStrategy):
         trade_date = timeframe_to_prev_date(trade.open_date_utc)
         # Look up trade candle.
         trade_candle = dataframe.loc[dataframe['date'] == trade_date]
-        # trade_candle may be None for trades that just opened as it is stil lincomplete.
+        # trade_candle may be None for trades that just opened as it is still incomplete.
         if trade_candle is not None:
             # <...>
 ```

From 5f5597b93fb43b8ed46bf1d1d2a88e4f3acf0b6a Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 06:32:55 +0200
Subject: [PATCH 365/460] Better test hyperopt writing and reading

---
 tests/optimize/test_hyperopt.py               |  85 +++++++-----------
 tests/optimize/test_optimize_reports.py       |   2 +-
 .../hyperopt_results_SampleStrategy.pickle    | Bin 0 -> 7436 bytes
 tests/testdata/strategy_SampleStrategy.fthypt |   5 ++
 4 files changed, 38 insertions(+), 54 deletions(-)
 create mode 100644 tests/testdata/hyperopt_results_SampleStrategy.pickle
 create mode 100644 tests/testdata/strategy_SampleStrategy.fthypt

diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index f9a9dba85..a0c475efb 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -31,23 +31,7 @@ from .hyperopts.default_hyperopt import DefaultHyperOpt
 
 
 # Functions for recurrent object patching
-def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
-    """
-    When creating results, mock the hyperopt so that *by default*
-      - we don't create any pickle'd files in the filesystem
-      - we might have a pickle'd file so make sure that we return
-        false when looking for it
-    """
-    hyperopt.results_file = testdatadir / 'optimize/ut_results.pickle'
-
-    mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
-    stat_mock = MagicMock()
-    stat_mock.st_size = 1
-    mocker.patch.object(Path, "stat", MagicMock(return_value=stat_mock))
-
-    mocker.patch.object(Path, "unlink", MagicMock(return_value=True))
-    mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
-    mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
+def create_results() -> List[Dict]:
 
     return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}]
 
@@ -321,54 +305,49 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
     assert caplog.record_tuples == []
 
 
-def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
-    epochs = create_results(mocker, hyperopt, testdatadir)
-    mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
-    mock_dump_json = mocker.patch('freqtrade.optimize.hyperopt.file_dump_json', return_value=None)
-    results_file = testdatadir / 'optimize' / 'ut_results.pickle'
+def test_save_results_saves_epochs(mocker, hyperopt, tmpdir, caplog) -> None:
+    # Test writing to temp dir and reading again
+    epochs = create_results()
+    hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
 
     caplog.set_level(logging.DEBUG)
 
-    hyperopt.epochs = epochs
-    hyperopt._save_results()
-    assert log_has(f"1 epoch saved to '{results_file}'.", caplog)
-    mock_dump.assert_called_once()
-    mock_dump_json.assert_called_once()
+    for epoch in epochs:
+        hyperopt._save_result(epoch)
+    assert log_has(f"1 epoch saved to '{hyperopt.results_file}'.", caplog)
 
-    hyperopt.epochs = epochs + epochs
-    hyperopt._save_results()
-    assert log_has(f"2 epochs saved to '{results_file}'.", caplog)
+    hyperopt._save_result(epochs[0])
+    assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
+
+    hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file)
+    assert len(hyperopt_epochs) == 2
 
 
-def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
-    epochs = create_results(mocker, hyperopt, testdatadir)
-    mock_load = mocker.patch('joblib.load', return_value=epochs)
-    results_file = testdatadir / 'optimize' / 'ut_results.pickle'
-    hyperopt_epochs = HyperoptTools._read_results(results_file)
-    assert log_has(f"Reading epochs from '{results_file}'", caplog)
-    assert hyperopt_epochs == epochs
-    mock_load.assert_called_once()
+def test_load_previous_results(testdatadir, caplog) -> None:
 
-
-def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
-    epochs = create_results(mocker, hyperopt, testdatadir)
-    mock_load = mocker.patch('joblib.load', return_value=epochs)
-    mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
-    statmock = MagicMock()
-    statmock.st_size = 5
-    # mocker.patch.object(Path, 'stat', MagicMock(return_value=statmock))
-
-    results_file = testdatadir / 'optimize' / 'ut_results.pickle'
+    results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
 
     hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
 
-    assert hyperopt_epochs == epochs
-    mock_load.assert_called_once()
+    assert len(hyperopt_epochs) == 5
+    assert log_has_re(r"Reading pickled epochs from .*", caplog)
 
-    del epochs[0]['is_best']
-    mock_load = mocker.patch('joblib.load', return_value=epochs)
+    caplog.clear()
 
-    with pytest.raises(OperationalException):
+    # Modern version
+    results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
+
+    hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
+
+    assert len(hyperopt_epochs) == 5
+    assert log_has_re(r"Reading epochs from .*", caplog)
+
+
+def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
+    mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
+                 return_value=[{'asdf': '222'}])
+    results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
+    with pytest.raises(OperationalException, match=r"The file .* incompatible.*"):
         HyperoptTools.load_previous_results(results_file)
 
 
diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py
index 48ba416c0..1afa5c59c 100644
--- a/tests/optimize/test_optimize_reports.py
+++ b/tests/optimize/test_optimize_reports.py
@@ -1,5 +1,5 @@
 import re
-from datetime import datetime, timedelta, timezone
+from datetime import timedelta
 from pathlib import Path
 
 import pandas as pd
diff --git a/tests/testdata/hyperopt_results_SampleStrategy.pickle b/tests/testdata/hyperopt_results_SampleStrategy.pickle
new file mode 100644
index 0000000000000000000000000000000000000000..2231de7bf15c53c0a36380d1a158dd1bd08ee9d9
GIT binary patch
literal 7436
zcmb`MdvH|M9mh8hLLe_F5DZ`OSVr42xO0=-dKWXm;V>Ph_u-fYI}2W2_2A4-Gcp;k}f`;=+t^e#i+cw
zcnsq(itpUOm}^+G6xYX0zinsCB@mJj;oHMPrz$IDw6{z{2|V5K5uTR$I5HuHy1NBQ
zRZ8Q$6qJNsL5lNXRVj}P;c#7Mx@gyHnXbnWA9Hxi7%%#y>=?gg;gcQcQpSh&#zy-!
z!wB@CQVR8CiVyEo>gmHq&g2k!O}D
z`CWz$g(|GJ)3)%3UmpDI$BXH9BE7~)uWil6w>p16vuN`!#zuO@Hfp+{*^1+dXpEc<
z91FId|Hb}g{hz-*K%SaZ%JrinB=}G`PEJcfd$JGSpb=;s8jmKTN$6HI1x-hG8CA)s
zT1GQuG!xY%l|q<0;yMe>L32?Zav(S2kPp?%s3C>?G789O0a}<;3h<1AXPb?V!*}=D
z*3$f6(ov!o+Bm83&hokwN3}+BW?aTOM62k6bQl}kY)K^(+QJTBK^BsL^h?kGb!0W0qK!DPvW}!A36Vrl
zh($Y)+Lot`3B`kdt+(tTmoU%CmArC}B8~;+vpYMH$vFULA+v+pZ9{%LGX3JoQ?%6K#
zdS@UPxNbv_qQ}qK4{GYE52=8dqpM_cKqK#gw!dT!^
zkM}En6#1xNtM(9`z{U>hn<#ZMEY3XJb$utb4o2lCVf6LhG?h8d=?OSJ!*bc>@c1ma
z47|n7CN8_(oTnk+(YefV4zCrL-CTp;;nKkT1-gMYx8N(vzh6u0h&!eETG3RBW3~3W@HLs
zQXgYL-#GZ$l@;y#^g{~uc)I!x5WF2?Y&xJRPP
zL8gLC1E~V32B`s=g(W4Q5LqZ7mh;sNo3G=TU)7J@WlNr{WF
z%HnP0{bHg`Sj8n+%+P=D3&?3&-7l`dJ``G$$u9(_=xouWIN!J^;%e%Ok7&r)o{fG{
zg1*;b$t?jqt0w?QIstq)lK>`KCjf6fhNmAA0B4y1j#P$U{=@C3=>lfg*)(1)h9MHb
z-}xQ4-Lqwr_7FA!+_ksG&Kzs<6a4W3$=C%SY4Tz)g=jJ>*U#2qr
zKUH^JJlS5T&BySRuP|;Qt1`TbaWGMSB5TeDU?pQ_C~vw1$({0JEoxBfdYpyyÐB
z<@kT%UPrqz#px-)hTMB$U?rEeLyi#ZrDMvn(v-Ez0+F84fi5
z;Q6O1n`nP&%3IlV%FiPpKcIatyA0p{>BhyryRXpmJ@g%ixCc(eUXXnt2SA&}_
zki#HHu*l+3tSRv&qCX{i46Aq?i;?mtpz#z)KgiEO&VsxOat`D?$m>{A;sv5_V3ox;
ziC)AizJ~5vg}T2^Eaxm5X3&S&4g3|8g4I9I^s^XXN--Rp2$
zG2Z71I6Sn-7f0vg1uVmY8DB^kFP7uwG?D!~=eC{y{+nM8&`dWx*(VvdlNH&gF%BlO
zv)><;O?KAIkll2_l0|lzy;BLG{hJLRUDUpp4fQH~d9nS|SLk*f
z2I|w{2vmd20GS0c8)PoXJP-$n6H7{T5p`pgMUJQktLVjIgt`wJ{U8C5Mvz4yO(07^
zmVzw9k`iynDvK>dzd^JWtGFDC8R{@!(;n6?!^JzX4+T-N9kxPe?<3bO!2$)==kw=WcR4?zMvYf-h%3GhZhQZ)7_2B>+{z8y=kiB0=?d%a`$lcj(|q>M_>gXhvnYfHiA-J
J1*!{<{{b0q;2i(}

literal 0
HcmV?d00001

diff --git a/tests/testdata/strategy_SampleStrategy.fthypt b/tests/testdata/strategy_SampleStrategy.fthypt
new file mode 100644
index 000000000..6dc2b9ab1
--- /dev/null
+++ b/tests/testdata/strategy_SampleStrategy.fthypt
@@ -0,0 +1,5 @@
+{"loss":100000,"params_dict":{"mfi-value":"20","fastd-value":"21","adx-value":"26","rsi-value":"23","mfi-enabled":true,"fastd-enabled":false,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal","sell-mfi-value":"97","sell-fastd-value":"85","sell-adx-value":"55","sell-rsi-value":"76","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"34","roi_t2":"28","roi_t3":"32","roi_p1":0.031,"roi_p2":0.033,"roi_p3":0.146,"stoploss":-0.05},"params_details":{"buy":{"mfi-value":"20","fastd-value":"21","adx-value":"26","rsi-value":"23","mfi-enabled":true,"fastd-enabled":false,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"97","sell-fastd-value":"85","sell-adx-value":"55","sell-rsi-value":"76","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.21, 32: 0.064, 60: 0.031, 94: 0}","stoploss":{"stoploss":-0.05}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[],"locks":[],"best_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"worst_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"results_per_pair":[{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TRX/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"sell_reason_summary":[],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":0,"total_volume":0.0,"avg_stake_amount":0,"profit_mean":0,"profit_median":0,"profit_total":0.0,"profit_total_abs":0,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793107,"trades_per_day":0.0,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0,"backtest_worst_day":0,"backtest_best_day_abs":0,"backtest_worst_day_abs":0,"winning_days":0,"draw_days":0,"losing_days":0,"wins":0,"losses":0,"draws":0,"holding_avg":"0:00:00","winner_holding_avg":"0:00:00","loser_holding_avg":"0:00:00","max_drawdown":0.0,"max_drawdown_abs":0.0,"max_drawdown_low":0.0,"max_drawdown_high":0.0,"drawdown_start":"1970-01-01 00:00:00+00:00","drawdown_start_ts":0,"drawdown_end":"1970-01-01 00:00:00+00:00","drawdown_end_ts":0,"csum_min":0,"csum_max":0},"results_explanation":"     0 trades. 0/0/0 Wins/Draws/Losses. Avg profit   0.00%. Median profit   0.00%. Total profit  0.00000000 BTC (   0.00\u03A3%). Avg duration 0:00:00 min.","total_profit":0.0,"current_epoch":1,"is_initial_point":true,"is_best":false}
+{"loss":100000,"params_dict":{"mfi-value":"14","fastd-value":"43","adx-value":"30","rsi-value":"24","mfi-enabled":true,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal","sell-mfi-value":"97","sell-fastd-value":"71","sell-adx-value":"82","sell-rsi-value":"99","sell-mfi-enabled":false,"sell-fastd-enabled":false,"sell-adx-enabled":false,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"84","roi_t2":"35","roi_t3":"19","roi_p1":0.024,"roi_p2":0.022,"roi_p3":0.061,"stoploss":-0.083},"params_details":{"buy":{"mfi-value":"14","fastd-value":"43","adx-value":"30","rsi-value":"24","mfi-enabled":true,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":true,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"97","sell-fastd-value":"71","sell-adx-value":"82","sell-rsi-value":"99","sell-mfi-enabled":false,"sell-fastd-enabled":false,"sell-adx-enabled":false,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.107, 19: 0.046, 54: 0.024, 138: 0}","stoploss":{"stoploss":-0.083}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[],"locks":[],"best_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"worst_pair":{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},"results_per_pair":[{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TRX/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"sell_reason_summary":[],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0,"profit_sum_pct":0.0,"profit_total_abs":0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":0,"total_volume":0.0,"avg_stake_amount":0,"profit_mean":0,"profit_median":0,"profit_total":0.0,"profit_total_abs":0,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.0,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0,"backtest_worst_day":0,"backtest_best_day_abs":0,"backtest_worst_day_abs":0,"winning_days":0,"draw_days":0,"losing_days":0,"wins":0,"losses":0,"draws":0,"holding_avg":"0:00:00","winner_holding_avg":"0:00:00","loser_holding_avg":"0:00:00","max_drawdown":0.0,"max_drawdown_abs":0.0,"max_drawdown_low":0.0,"max_drawdown_high":0.0,"drawdown_start":"1970-01-01 00:00:00+00:00","drawdown_start_ts":0,"drawdown_end":"1970-01-01 00:00:00+00:00","drawdown_end_ts":0,"csum_min":0,"csum_max":0},"results_explanation":"     0 trades. 0/0/0 Wins/Draws/Losses. Avg profit   0.00%. Median profit   0.00%. Total profit  0.00000000 BTC (   0.00\u03A3%). Avg duration 0:00:00 min.","total_profit":0.0,"current_epoch":2,"is_initial_point":true,"is_best":false}
+{"loss":2.183447401951895,"params_dict":{"mfi-value":"14","fastd-value":"15","adx-value":"40","rsi-value":"36","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":false,"trigger":"sar_reversal","sell-mfi-value":"92","sell-fastd-value":"84","sell-adx-value":"61","sell-rsi-value":"61","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper","roi_t1":"68","roi_t2":"41","roi_t3":"21","roi_p1":0.015,"roi_p2":0.064,"roi_p3":0.126,"stoploss":-0.024},"params_details":{"buy":{"mfi-value":"14","fastd-value":"15","adx-value":"40","rsi-value":"36","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":false,"rsi-enabled":false,"trigger":"sar_reversal"},"sell":{"sell-mfi-value":"92","sell-fastd-value":"84","sell-adx-value":"61","sell-rsi-value":"61","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":true,"sell-rsi-enabled":true,"sell-trigger":"sell-bb_upper"},"roi":"{0: 0.20500000000000002, 21: 0.079, 62: 0.015, 130: 0}","stoploss":{"stoploss":-0.024}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.94115571,"open_date":"2018-01-11 11:40:00+00:00","close_date":"2018-01-11 19:40:00+00:00","open_rate":0.01700012,"close_rate":0.017119538805820372,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":480,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.01659211712,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.01659211712,"stop_loss_ratio":-0.024,"min_rate":0.01689809,"max_rate":0.0171462,"is_open":false,"open_timestamp":1515670800000.0,"close_timestamp":1515699600000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.57407318,"open_date":"2018-01-12 11:05:00+00:00","close_date":"2018-01-12 12:30:00+00:00","open_rate":0.08709691,"close_rate":0.08901977203712995,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":0.08500658416,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.08500658416,"stop_loss_ratio":-0.024,"min_rate":0.08702974000000001,"max_rate":0.08929248000000001,"is_open":false,"open_timestamp":1515755100000.0,"close_timestamp":1515760200000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.93166236,"open_date":"2018-01-12 03:30:00+00:00","close_date":"2018-01-12 13:05:00+00:00","open_rate":0.01705517,"close_rate":0.01717497550928249,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":575,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016645845920000003,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016645845920000003,"stop_loss_ratio":-0.024,"min_rate":0.0169841,"max_rate":0.01719135,"is_open":false,"open_timestamp":1515727800000.0,"close_timestamp":1515762300000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.96876855,"open_date":"2018-01-13 03:50:00+00:00","close_date":"2018-01-13 06:05:00+00:00","open_rate":0.016842,"close_rate":0.016960308078273957,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016437792,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016437792,"stop_loss_ratio":-0.024,"min_rate":0.016836999999999998,"max_rate":0.017,"is_open":false,"open_timestamp":1515815400000.0,"close_timestamp":1515823500000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.53163205,"open_date":"2018-01-13 13:25:00+00:00","close_date":"2018-01-13 15:35:00+00:00","open_rate":0.09405001,"close_rate":0.09471067238835926,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":130,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.09179280976,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.09179280976,"stop_loss_ratio":-0.024,"min_rate":0.09369894000000001,"max_rate":0.09479997,"is_open":false,"open_timestamp":1515849900000.0,"close_timestamp":1515857700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.23816853,"open_date":"2018-01-13 15:30:00+00:00","close_date":"2018-01-13 16:20:00+00:00","open_rate":0.0025989999999999997,"close_rate":0.0028232990466633217,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":50,"profit_ratio":0.07872446,"profit_abs":0.00395,"sell_reason":"roi","initial_stop_loss_abs":0.002536624,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.002536624,"stop_loss_ratio":-0.024,"min_rate":0.00259525,"max_rate":0.0028288700000000003,"is_open":false,"open_timestamp":1515857400000.0,"close_timestamp":1515860400000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":492.80504632,"open_date":"2018-01-14 21:35:00+00:00","close_date":"2018-01-14 23:15:00+00:00","open_rate":0.00010146000000000001,"close_rate":0.00010369995985950828,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":100,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":9.902496e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.902496e-05,"stop_loss_ratio":-0.024,"min_rate":0.0001012,"max_rate":0.00010414,"is_open":false,"open_timestamp":1515965700000.0,"close_timestamp":1515971700000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.92398174,"open_date":"2018-01-15 12:45:00+00:00","close_date":"2018-01-15 21:05:00+00:00","open_rate":0.01709997,"close_rate":0.01722009021073758,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":500,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.016689570719999998,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016689570719999998,"stop_loss_ratio":-0.024,"min_rate":0.01694,"max_rate":0.01725,"is_open":false,"open_timestamp":1516020300000.0,"close_timestamp":1516050300000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1111.60515785,"open_date":"2018-01-15 19:50:00+00:00","close_date":"2018-01-15 23:45:00+00:00","open_rate":4.4980000000000006e-05,"close_rate":4.390048e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":235,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":4.390048e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":4.390048e-05,"stop_loss_ratio":-0.024,"min_rate":4.409e-05,"max_rate":4.502e-05,"is_open":false,"open_timestamp":1516045800000.0,"close_timestamp":1516059900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":519.80455349,"open_date":"2018-01-21 03:55:00+00:00","close_date":"2018-01-21 04:05:00+00:00","open_rate":9.619e-05,"close_rate":9.388144e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":10,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":9.388144e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.388144e-05,"stop_loss_ratio":-0.024,"min_rate":9.568e-05,"max_rate":9.673e-05,"is_open":false,"open_timestamp":1516506900000.0,"close_timestamp":1516507500000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.029754,"open_date":"2018-01-20 22:15:00+00:00","close_date":"2018-01-21 07:45:00+00:00","open_rate":0.01650299,"close_rate":0.016106918239999997,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":570,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.016106918239999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016106918239999997,"stop_loss_ratio":-0.024,"min_rate":0.0162468,"max_rate":0.01663179,"is_open":false,"open_timestamp":1516486500000.0,"close_timestamp":1516520700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.75461832,"open_date":"2018-01-21 13:00:00+00:00","close_date":"2018-01-21 16:25:00+00:00","open_rate":0.00266601,"close_rate":0.00260202576,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":205,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.00260202576,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.00260202576,"stop_loss_ratio":-0.024,"min_rate":0.0026290800000000002,"max_rate":0.00269384,"is_open":false,"open_timestamp":1516539600000.0,"close_timestamp":1516551900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":552.18111541,"open_date":"2018-01-22 02:10:00+00:00","close_date":"2018-01-22 04:20:00+00:00","open_rate":9.055e-05,"close_rate":9.118607626693427e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":130,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":8.83768e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":8.83768e-05,"stop_loss_ratio":-0.024,"min_rate":9.013e-05,"max_rate":9.197e-05,"is_open":false,"open_timestamp":1516587000000.0,"close_timestamp":1516594800000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":2.99733237,"open_date":"2018-01-22 03:20:00+00:00","close_date":"2018-01-22 13:50:00+00:00","open_rate":0.0166815,"close_rate":0.016281143999999997,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":630,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":0.016281143999999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.016281143999999997,"stop_loss_ratio":-0.024,"min_rate":0.01641443,"max_rate":0.016800000000000002,"is_open":false,"open_timestamp":1516591200000.0,"close_timestamp":1516629000000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":503.52467271,"open_date":"2018-01-23 08:55:00+00:00","close_date":"2018-01-23 09:40:00+00:00","open_rate":9.93e-05,"close_rate":9.69168e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":45,"profit_ratio":-0.03080817,"profit_abs":-0.0015458,"sell_reason":"stop_loss","initial_stop_loss_abs":9.69168e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":9.69168e-05,"stop_loss_ratio":-0.024,"min_rate":9.754e-05,"max_rate":0.00010025,"is_open":false,"open_timestamp":1516697700000.0,"close_timestamp":1516700400000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.55148073,"open_date":"2018-01-24 02:10:00+00:00","close_date":"2018-01-24 04:40:00+00:00","open_rate":0.090665,"close_rate":0.09130188409433015,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":150,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.08848903999999999,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.08848903999999999,"stop_loss_ratio":-0.024,"min_rate":0.090665,"max_rate":0.09146000000000001,"is_open":false,"open_timestamp":1516759800000.0,"close_timestamp":1516768800000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.10584639,"open_date":"2018-01-24 19:20:00+00:00","close_date":"2018-01-24 21:35:00+00:00","open_rate":0.002617,"close_rate":0.0026353833416959357,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.002554192,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.002554192,"stop_loss_ratio":-0.024,"min_rate":0.002617,"max_rate":0.00264999,"is_open":false,"open_timestamp":1516821600000.0,"close_timestamp":1516829700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.34602691,"open_date":"2018-01-25 14:35:00+00:00","close_date":"2018-01-25 16:35:00+00:00","open_rate":0.00258451,"close_rate":0.002641568926241846,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.01494768,"profit_abs":0.00075,"sell_reason":"roi","initial_stop_loss_abs":0.00252248176,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.00252248176,"stop_loss_ratio":-0.024,"min_rate":0.00258451,"max_rate":0.00264579,"is_open":false,"open_timestamp":1516890900000.0,"close_timestamp":1516898100000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.11910295,"open_date":"2018-01-24 23:05:00+00:00","close_date":"2018-01-25 16:55:00+00:00","open_rate":0.01603025,"close_rate":0.016142855870546913,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":1070,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.015645523999999997,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.015645523999999997,"stop_loss_ratio":-0.024,"min_rate":0.015798760000000002,"max_rate":0.01617,"is_open":false,"open_timestamp":1516835100000.0,"close_timestamp":1516899300000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":553.70985604,"open_date":"2018-01-26 19:30:00+00:00","close_date":"2018-01-26 23:30:00+00:00","open_rate":9.03e-05,"close_rate":9.093432012042147e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":240,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":8.813279999999999e-05,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":8.813279999999999e-05,"stop_loss_ratio":-0.024,"min_rate":8.961e-05,"max_rate":9.1e-05,"is_open":false,"open_timestamp":1516995000000.0,"close_timestamp":1517009400000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":19.22929005,"open_date":"2018-01-26 21:15:00+00:00","close_date":"2018-01-28 03:50:00+00:00","open_rate":0.0026002,"close_rate":0.0026184653286502758,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":1835,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0025377952,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.0025377952,"stop_loss_ratio":-0.024,"min_rate":0.00254702,"max_rate":0.00262797,"is_open":false,"open_timestamp":1517001300000.0,"close_timestamp":1517111400000.0},{"pair":"LTC/BTC","stake_amount":0.05,"amount":3.15677093,"open_date":"2018-01-27 22:05:00+00:00","close_date":"2018-01-28 10:45:00+00:00","open_rate":0.01583897,"close_rate":0.015950232207727046,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":760,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.01545883472,"initial_stop_loss_ratio":-0.024,"stop_loss_abs":0.01545883472,"stop_loss_ratio":-0.024,"min_rate":0.015700000000000002,"max_rate":0.01596521,"is_open":false,"open_timestamp":1517090700000.0,"close_timestamp":1517136300000.0}],"locks":[],"best_pair":{"key":"ETC/BTC","trades":5,"profit_mean":0.012572794000000002,"profit_mean_pct":1.2572794000000003,"profit_sum":0.06286397,"profit_sum_pct":6.29,"profit_total_abs":0.0031542000000000002,"profit_total":3.1542000000000002e-06,"profit_total_pct":0.0,"duration_avg":"7:49:00","wins":2,"draws":2,"losses":1},"worst_pair":{"key":"LTC/BTC","trades":8,"profit_mean":-0.0077020425,"profit_mean_pct":-0.77020425,"profit_sum":-0.06161634,"profit_sum_pct":-6.16,"profit_total_abs":-0.0030916,"profit_total":-3.0915999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"9:50:00","wins":0,"draws":6,"losses":2},"results_per_pair":[{"key":"ETC/BTC","trades":5,"profit_mean":0.012572794000000002,"profit_mean_pct":1.2572794000000003,"profit_sum":0.06286397,"profit_sum_pct":6.29,"profit_total_abs":0.0031542000000000002,"profit_total":3.1542000000000002e-06,"profit_total_pct":0.0,"duration_avg":"7:49:00","wins":2,"draws":2,"losses":1},{"key":"ETH/BTC","trades":3,"profit_mean":0.00498256,"profit_mean_pct":0.498256,"profit_sum":0.01494768,"profit_sum_pct":1.49,"profit_total_abs":0.00075,"profit_total":7.5e-07,"profit_total_pct":0.0,"duration_avg":"2:02:00","wins":1,"draws":2,"losses":0},{"key":"ADA/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":1,"profit_mean":-0.03080817,"profit_mean_pct":-3.080817,"profit_sum":-0.03080817,"profit_sum_pct":-3.08,"profit_total_abs":-0.0015458,"profit_total":-1.5457999999999999e-06,"profit_total_pct":-0.0,"duration_avg":"3:55:00","wins":0,"draws":0,"losses":1},{"key":"TRX/BTC","trades":5,"profit_mean":-0.009333732,"profit_mean_pct":-0.9333732000000001,"profit_sum":-0.04666866,"profit_sum_pct":-4.67,"profit_total_abs":-0.0023416,"profit_total":-2.3416e-06,"profit_total_pct":-0.0,"duration_avg":"1:45:00","wins":1,"draws":2,"losses":2},{"key":"LTC/BTC","trades":8,"profit_mean":-0.0077020425,"profit_mean_pct":-0.77020425,"profit_sum":-0.06161634,"profit_sum_pct":-6.16,"profit_total_abs":-0.0030916,"profit_total":-3.0915999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"9:50:00","wins":0,"draws":6,"losses":2},{"key":"TOTAL","trades":22,"profit_mean":-0.0027855236363636365,"profit_mean_pct":-0.27855236363636365,"profit_sum":-0.06128152,"profit_sum_pct":-6.13,"profit_total_abs":-0.0030748,"profit_total":-3.0747999999999998e-06,"profit_total_pct":-0.0,"duration_avg":"6:12:00","wins":4,"draws":12,"losses":6}],"sell_reason_summary":[{"sell_reason":"roi","trades":16,"wins":4,"draws":12,"losses":0,"profit_mean":0.00772296875,"profit_mean_pct":0.77,"profit_sum":0.1235675,"profit_sum_pct":12.36,"profit_total_abs":0.006200000000000001,"profit_total":0.041189166666666666,"profit_total_pct":4.12},{"sell_reason":"stop_loss","trades":6,"wins":0,"draws":0,"losses":6,"profit_mean":-0.03080817,"profit_mean_pct":-3.08,"profit_sum":-0.18484902,"profit_sum_pct":-18.48,"profit_total_abs":-0.0092748,"profit_total":-0.06161634,"profit_total_pct":-6.16}],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":22,"total_volume":1.1000000000000003,"avg_stake_amount":0.05000000000000002,"profit_mean":-0.0027855236363636365,"profit_median":0.0,"profit_total":-3.0747999999999998e-06,"profit_total_abs":-0.0030748,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":1.16,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":999.9969252,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.07872446,"backtest_worst_day":-0.09242451,"backtest_best_day_abs":0.00395,"backtest_worst_day_abs":-0.0046374,"winning_days":4,"draw_days":10,"losing_days":4,"wins":4,"losses":6,"draws":12,"holding_avg":"6:12:00","winner_holding_avg":"1:29:00","loser_holding_avg":"4:42:00","max_drawdown":0.18484901999999998,"max_drawdown_abs":0.0092748,"drawdown_start":"2018-01-14 23:15:00","drawdown_start_ts":1515971700000.0,"drawdown_end":"2018-01-23 09:40:00","drawdown_end_ts":1516700400000.0,"max_drawdown_low":-0.0038247999999999997,"max_drawdown_high":0.00545,"csum_min":999.9961752,"csum_max":1000.00545},"results_explanation":"    22 trades. 4/12/6 Wins/Draws/Losses. Avg profit  -0.28%. Median profit   0.00%. Total profit -0.00307480 BTC (  -0.00\u03A3%). Avg duration 6:12:00 min.","total_profit":-3.0747999999999998e-06,"current_epoch":3,"is_initial_point":true,"is_best":true}
+{"loss":-4.9544427978437175,"params_dict":{"mfi-value":"23","fastd-value":"40","adx-value":"50","rsi-value":"27","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":true,"rsi-enabled":true,"trigger":"bb_lower","sell-mfi-value":"87","sell-fastd-value":"60","sell-adx-value":"81","sell-rsi-value":"69","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":false,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal","roi_t1":"105","roi_t2":"43","roi_t3":"12","roi_p1":0.03,"roi_p2":0.036,"roi_p3":0.103,"stoploss":-0.081},"params_details":{"buy":{"mfi-value":"23","fastd-value":"40","adx-value":"50","rsi-value":"27","mfi-enabled":false,"fastd-enabled":true,"adx-enabled":true,"rsi-enabled":true,"trigger":"bb_lower"},"sell":{"sell-mfi-value":"87","sell-fastd-value":"60","sell-adx-value":"81","sell-rsi-value":"69","sell-mfi-enabled":true,"sell-fastd-enabled":true,"sell-adx-enabled":false,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal"},"roi":"{0: 0.16899999999999998, 12: 0.066, 55: 0.03, 160: 0}","stoploss":{"stoploss":-0.081}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"XLM/BTC","stake_amount":0.05,"amount":1086.95652174,"open_date":"2018-01-13 13:30:00+00:00","close_date":"2018-01-13 16:30:00+00:00","open_rate":4.6e-05,"close_rate":4.632313095835424e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":180,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.2274e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.2274e-05,"stop_loss_ratio":-0.081,"min_rate":4.4980000000000006e-05,"max_rate":4.673e-05,"is_open":false,"open_timestamp":1515850200000.0,"close_timestamp":1515861000000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":851.35365231,"open_date":"2018-01-15 14:50:00+00:00","close_date":"2018-01-15 16:15:00+00:00","open_rate":5.873000000000001e-05,"close_rate":6.0910642247867544e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":5.397287000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.397287000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.873000000000001e-05,"max_rate":6.120000000000001e-05,"is_open":false,"open_timestamp":1516027800000.0,"close_timestamp":1516032900000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":896.86098655,"open_date":"2018-01-16 00:35:00+00:00","close_date":"2018-01-16 03:15:00+00:00","open_rate":5.575000000000001e-05,"close_rate":5.6960000000000004e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.01457705,"profit_abs":0.0007314,"sell_reason":"roi","initial_stop_loss_abs":5.123425000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.123425000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.575000000000001e-05,"max_rate":5.730000000000001e-05,"is_open":false,"open_timestamp":1516062900000.0,"close_timestamp":1516072500000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":747.160789,"open_date":"2018-01-16 22:30:00+00:00","close_date":"2018-01-16 22:45:00+00:00","open_rate":6.692e-05,"close_rate":7.182231811339689e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":15,"profit_ratio":0.06576981,"profit_abs":0.0033,"sell_reason":"roi","initial_stop_loss_abs":6.149948000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":6.149948000000001e-05,"stop_loss_ratio":-0.081,"min_rate":6.692e-05,"max_rate":7.566e-05,"is_open":false,"open_timestamp":1516141800000.0,"close_timestamp":1516142700000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":720.5649229,"open_date":"2018-01-17 15:15:00+00:00","close_date":"2018-01-17 16:40:00+00:00","open_rate":6.939000000000001e-05,"close_rate":7.19664475664827e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":85,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":6.376941000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":6.376941000000001e-05,"stop_loss_ratio":-0.081,"min_rate":6.758e-05,"max_rate":7.244e-05,"is_open":false,"open_timestamp":1516202100000.0,"close_timestamp":1516207200000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1144.42664225,"open_date":"2018-01-18 22:20:00+00:00","close_date":"2018-01-19 00:35:00+00:00","open_rate":4.3690000000000004e-05,"close_rate":4.531220772704466e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":135,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":4.015111e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.015111e-05,"stop_loss_ratio":-0.081,"min_rate":4.3690000000000004e-05,"max_rate":4.779e-05,"is_open":false,"open_timestamp":1516314000000.0,"close_timestamp":1516322100000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":876.57784011,"open_date":"2018-01-18 22:25:00+00:00","close_date":"2018-01-19 01:05:00+00:00","open_rate":5.704e-05,"close_rate":5.792e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.00834457,"profit_abs":0.00041869,"sell_reason":"roi","initial_stop_loss_abs":5.2419760000000006e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":5.2419760000000006e-05,"stop_loss_ratio":-0.081,"min_rate":5.704e-05,"max_rate":5.8670000000000006e-05,"is_open":false,"open_timestamp":1516314300000.0,"close_timestamp":1516323900000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":525.59655209,"open_date":"2018-01-20 05:05:00+00:00","close_date":"2018-01-20 06:25:00+00:00","open_rate":9.513e-05,"close_rate":9.86621726041144e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":80,"profit_ratio":0.02989537,"profit_abs":0.0015,"sell_reason":"roi","initial_stop_loss_abs":8.742447000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":8.742447000000001e-05,"stop_loss_ratio":-0.081,"min_rate":9.513e-05,"max_rate":9.95e-05,"is_open":false,"open_timestamp":1516424700000.0,"close_timestamp":1516429500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":920.64076597,"open_date":"2018-01-26 07:40:00+00:00","close_date":"2018-01-26 10:20:00+00:00","open_rate":5.431000000000001e-05,"close_rate":5.474000000000001e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":160,"profit_ratio":0.0008867,"profit_abs":4.449e-05,"sell_reason":"roi","initial_stop_loss_abs":4.991089000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.991089000000001e-05,"stop_loss_ratio":-0.081,"min_rate":5.3670000000000006e-05,"max_rate":5.5e-05,"is_open":false,"open_timestamp":1516952400000.0,"close_timestamp":1516962000000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":944.28706327,"open_date":"2018-01-28 04:35:00+00:00","close_date":"2018-01-30 04:45:00+00:00","open_rate":5.2950000000000006e-05,"close_rate":4.995000000000001e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":2890,"profit_ratio":-0.06323759,"profit_abs":-0.00317295,"sell_reason":"force_sell","initial_stop_loss_abs":4.866105000000001e-05,"initial_stop_loss_ratio":-0.081,"stop_loss_abs":4.866105000000001e-05,"stop_loss_ratio":-0.081,"min_rate":4.980000000000001e-05,"max_rate":5.3280000000000005e-05,"is_open":true,"open_timestamp":1517114100000.0,"close_timestamp":1517287500000.0}],"locks":[],"best_pair":{"key":"TRX/BTC","trades":3,"profit_mean":0.04185351666666667,"profit_mean_pct":4.185351666666667,"profit_sum":0.12556055,"profit_sum_pct":12.56,"profit_total_abs":0.0063,"profit_total":6.3e-06,"profit_total_pct":0.0,"duration_avg":"1:00:00","wins":3,"draws":0,"losses":0},"worst_pair":{"key":"XLM/BTC","trades":3,"profit_mean":-0.01111407333333333,"profit_mean_pct":-1.111407333333333,"profit_sum":-0.03334221999999999,"profit_sum_pct":-3.33,"profit_total_abs":-0.0016729499999999999,"profit_total":-1.6729499999999998e-06,"profit_total_pct":-0.0,"duration_avg":"17:48:00","wins":1,"draws":1,"losses":1},"results_per_pair":[{"key":"TRX/BTC","trades":3,"profit_mean":0.04185351666666667,"profit_mean_pct":4.185351666666667,"profit_sum":0.12556055,"profit_sum_pct":12.56,"profit_total_abs":0.0063,"profit_total":6.3e-06,"profit_total_pct":0.0,"duration_avg":"1:00:00","wins":3,"draws":0,"losses":0},{"key":"ADA/BTC","trades":4,"profit_mean":0.0134259225,"profit_mean_pct":1.34259225,"profit_sum":0.05370369,"profit_sum_pct":5.37,"profit_total_abs":0.00269458,"profit_total":2.69458e-06,"profit_total_pct":0.0,"duration_avg":"2:21:00","wins":4,"draws":0,"losses":0},{"key":"ETH/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"ETC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":3,"profit_mean":-0.01111407333333333,"profit_mean_pct":-1.111407333333333,"profit_sum":-0.03334221999999999,"profit_sum_pct":-3.33,"profit_total_abs":-0.0016729499999999999,"profit_total":-1.6729499999999998e-06,"profit_total_pct":-0.0,"duration_avg":"17:48:00","wins":1,"draws":1,"losses":1},{"key":"TOTAL","trades":10,"profit_mean":0.014592201999999999,"profit_mean_pct":1.4592201999999999,"profit_sum":0.14592201999999999,"profit_sum_pct":14.59,"profit_total_abs":0.00732163,"profit_total":7.32163e-06,"profit_total_pct":0.0,"duration_avg":"6:35:00","wins":8,"draws":1,"losses":1}],"sell_reason_summary":[{"sell_reason":"roi","trades":9,"wins":8,"draws":1,"losses":0,"profit_mean":0.023239956666666665,"profit_mean_pct":2.32,"profit_sum":0.20915961,"profit_sum_pct":20.92,"profit_total_abs":0.01049458,"profit_total":0.06971987,"profit_total_pct":6.97},{"sell_reason":"force_sell","trades":1,"wins":0,"draws":0,"losses":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.32,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-0.021079196666666664,"profit_total_pct":-2.11}],"left_open_trades":[{"key":"XLM/BTC","trades":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.323759,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-3.17295e-06,"profit_total_pct":-0.0,"duration_avg":"2 days, 0:10:00","wins":0,"draws":0,"losses":1},{"key":"TOTAL","trades":1,"profit_mean":-0.06323759,"profit_mean_pct":-6.323759,"profit_sum":-0.06323759,"profit_sum_pct":-6.32,"profit_total_abs":-0.00317295,"profit_total":-3.17295e-06,"profit_total_pct":-0.0,"duration_avg":"2 days, 0:10:00","wins":0,"draws":0,"losses":1}],"total_trades":10,"total_volume":0.5,"avg_stake_amount":0.05,"profit_mean":0.014592201999999999,"profit_median":0.02223621,"profit_total":7.32163e-06,"profit_total_abs":0.00732163,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.53,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000.00732163,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.08034685999999999,"backtest_worst_day":-0.06323759,"backtest_best_day_abs":0.0040314,"backtest_worst_day_abs":-0.00317295,"winning_days":6,"draw_days":11,"losing_days":1,"wins":8,"losses":1,"draws":1,"holding_avg":"6:35:00","winner_holding_avg":"1:50:00","loser_holding_avg":"2 days, 0:10:00","max_drawdown":0.06323759000000001,"max_drawdown_abs":0.00317295,"drawdown_start":"2018-01-26 10:20:00","drawdown_start_ts":1516962000000.0,"drawdown_end":"2018-01-30 04:45:00","drawdown_end_ts":1517287500000.0,"max_drawdown_low":0.007321629999999998,"max_drawdown_high":0.010494579999999998,"csum_min":1000.0,"csum_max":1000.01049458},"results_explanation":"    10 trades. 8/1/1 Wins/Draws/Losses. Avg profit   1.46%. Median profit   2.22%. Total profit  0.00732163 BTC (   0.00\u03A3%). Avg duration 6:35:00 min.","total_profit":7.32163e-06,"current_epoch":4,"is_initial_point":true,"is_best":true}
+{"loss":0.16709185414267655,"params_dict":{"mfi-value":"10","fastd-value":"45","adx-value":"28","rsi-value":"37","mfi-enabled":false,"fastd-enabled":false,"adx-enabled":true,"rsi-enabled":true,"trigger":"macd_cross_signal","sell-mfi-value":"85","sell-fastd-value":"56","sell-adx-value":"98","sell-rsi-value":"89","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal","roi_t1":"85","roi_t2":"11","roi_t3":"24","roi_p1":0.04,"roi_p2":0.043,"roi_p3":0.053,"stoploss":-0.057},"params_details":{"buy":{"mfi-value":"10","fastd-value":"45","adx-value":"28","rsi-value":"37","mfi-enabled":false,"fastd-enabled":false,"adx-enabled":true,"rsi-enabled":true,"trigger":"macd_cross_signal"},"sell":{"sell-mfi-value":"85","sell-fastd-value":"56","sell-adx-value":"98","sell-rsi-value":"89","sell-mfi-enabled":true,"sell-fastd-enabled":false,"sell-adx-enabled":true,"sell-rsi-enabled":false,"sell-trigger":"sell-sar_reversal"},"roi":"{0: 0.13599999999999998, 24: 0.08299999999999999, 35: 0.04, 120: 0}","stoploss":{"stoploss":-0.057}},"params_not_optimized":{"buy":{},"sell":{}},"results_metrics":{"trades":[{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.56173464,"open_date":"2018-01-10 19:15:00+00:00","close_date":"2018-01-10 21:15:00+00:00","open_rate":0.08901,"close_rate":0.09112999000000001,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.01667571,"profit_abs":0.0008367,"sell_reason":"roi","initial_stop_loss_abs":0.08393643,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.08393643,"stop_loss_ratio":-0.057,"min_rate":0.08894498,"max_rate":0.09116998,"is_open":false,"open_timestamp":1515611700000.0,"close_timestamp":1515618900000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":794.65988557,"open_date":"2018-01-13 11:30:00+00:00","close_date":"2018-01-13 15:10:00+00:00","open_rate":6.292e-05,"close_rate":5.9333559999999994e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":220,"profit_ratio":-0.06357798,"profit_abs":-0.00319003,"sell_reason":"stop_loss","initial_stop_loss_abs":5.9333559999999994e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.9333559999999994e-05,"stop_loss_ratio":-0.057,"min_rate":5.9900000000000006e-05,"max_rate":6.353e-05,"is_open":false,"open_timestamp":1515843000000.0,"close_timestamp":1515856200000.0},{"pair":"XLM/BTC","stake_amount":0.05,"amount":1086.95652174,"open_date":"2018-01-13 14:35:00+00:00","close_date":"2018-01-13 21:40:00+00:00","open_rate":4.6e-05,"close_rate":4.632313095835424e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":425,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.3378e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":4.3378e-05,"stop_loss_ratio":-0.057,"min_rate":4.4980000000000006e-05,"max_rate":4.6540000000000005e-05,"is_open":false,"open_timestamp":1515854100000.0,"close_timestamp":1515879600000.0},{"pair":"ETH/BTC","stake_amount":0.05,"amount":0.53757603,"open_date":"2018-01-15 13:15:00+00:00","close_date":"2018-01-15 15:15:00+00:00","open_rate":0.0930101,"close_rate":0.09366345745107878,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0877085243,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0877085243,"stop_loss_ratio":-0.057,"min_rate":0.09188489999999999,"max_rate":0.09380000000000001,"is_open":false,"open_timestamp":1516022100000.0,"close_timestamp":1516029300000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":17.07469496,"open_date":"2018-01-15 14:35:00+00:00","close_date":"2018-01-15 16:35:00+00:00","open_rate":0.00292831,"close_rate":0.00297503,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.00886772,"profit_abs":0.00044494,"sell_reason":"roi","initial_stop_loss_abs":0.0027613963299999997,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0027613963299999997,"stop_loss_ratio":-0.057,"min_rate":0.00292831,"max_rate":0.00301259,"is_open":false,"open_timestamp":1516026900000.0,"close_timestamp":1516034100000.0},{"pair":"TRX/BTC","stake_amount":0.05,"amount":702.44450688,"open_date":"2018-01-17 04:25:00+00:00","close_date":"2018-01-17 05:00:00+00:00","open_rate":7.118e-05,"close_rate":7.453721023582538e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":35,"profit_ratio":0.03986049,"profit_abs":0.002,"sell_reason":"roi","initial_stop_loss_abs":6.712274e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":6.712274e-05,"stop_loss_ratio":-0.057,"min_rate":7.118e-05,"max_rate":7.658000000000002e-05,"is_open":false,"open_timestamp":1516163100000.0,"close_timestamp":1516165200000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.86756854,"open_date":"2018-01-20 06:05:00+00:00","close_date":"2018-01-20 08:05:00+00:00","open_rate":0.00265005,"close_rate":0.00266995,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":120,"profit_ratio":0.00048133,"profit_abs":2.415e-05,"sell_reason":"roi","initial_stop_loss_abs":0.00249899715,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.00249899715,"stop_loss_ratio":-0.057,"min_rate":0.00265005,"max_rate":0.00271,"is_open":false,"open_timestamp":1516428300000.0,"close_timestamp":1516435500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":966.18357488,"open_date":"2018-01-22 03:25:00+00:00","close_date":"2018-01-22 07:05:00+00:00","open_rate":5.1750000000000004e-05,"close_rate":5.211352232814853e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":220,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":4.8800250000000004e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":4.8800250000000004e-05,"stop_loss_ratio":-0.057,"min_rate":5.1750000000000004e-05,"max_rate":5.2170000000000004e-05,"is_open":false,"open_timestamp":1516591500000.0,"close_timestamp":1516604700000.0},{"pair":"ETC/BTC","stake_amount":0.05,"amount":18.95303438,"open_date":"2018-01-23 13:10:00+00:00","close_date":"2018-01-23 16:00:00+00:00","open_rate":0.0026381,"close_rate":0.002656631560461616,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":170,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":0.0024877283,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":0.0024877283,"stop_loss_ratio":-0.057,"min_rate":0.0026100000000000003,"max_rate":0.00266,"is_open":false,"open_timestamp":1516713000000.0,"close_timestamp":1516723200000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":912.40875912,"open_date":"2018-01-26 06:30:00+00:00","close_date":"2018-01-26 10:45:00+00:00","open_rate":5.480000000000001e-05,"close_rate":5.518494731560462e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":255,"profit_ratio":-0.0,"profit_abs":-0.0,"sell_reason":"roi","initial_stop_loss_abs":5.1676400000000006e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.1676400000000006e-05,"stop_loss_ratio":-0.057,"min_rate":5.3670000000000006e-05,"max_rate":5.523e-05,"is_open":false,"open_timestamp":1516948200000.0,"close_timestamp":1516963500000.0},{"pair":"ADA/BTC","stake_amount":0.05,"amount":909.58704748,"open_date":"2018-01-27 02:10:00+00:00","close_date":"2018-01-27 05:40:00+00:00","open_rate":5.4970000000000004e-05,"close_rate":5.535614149523332e-05,"fee_open":0.0035,"fee_close":0.0035,"trade_duration":210,"profit_ratio":0.0,"profit_abs":0.0,"sell_reason":"roi","initial_stop_loss_abs":5.183671e-05,"initial_stop_loss_ratio":-0.057,"stop_loss_abs":5.183671e-05,"stop_loss_ratio":-0.057,"min_rate":5.472000000000001e-05,"max_rate":5.556e-05,"is_open":false,"open_timestamp":1517019000000.0,"close_timestamp":1517031600000.0}],"locks":[],"best_pair":{"key":"TRX/BTC","trades":1,"profit_mean":0.03986049,"profit_mean_pct":3.986049,"profit_sum":0.03986049,"profit_sum_pct":3.99,"profit_total_abs":0.002,"profit_total":2e-06,"profit_total_pct":0.0,"duration_avg":"0:35:00","wins":1,"draws":0,"losses":0},"worst_pair":{"key":"ADA/BTC","trades":4,"profit_mean":-0.015894495,"profit_mean_pct":-1.5894495000000002,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-3.19003e-06,"profit_total_pct":-0.0,"duration_avg":"3:46:00","wins":0,"draws":3,"losses":1},"results_per_pair":[{"key":"TRX/BTC","trades":1,"profit_mean":0.03986049,"profit_mean_pct":3.986049,"profit_sum":0.03986049,"profit_sum_pct":3.99,"profit_total_abs":0.002,"profit_total":2e-06,"profit_total_pct":0.0,"duration_avg":"0:35:00","wins":1,"draws":0,"losses":0},{"key":"ETH/BTC","trades":2,"profit_mean":0.008337855,"profit_mean_pct":0.8337855,"profit_sum":0.01667571,"profit_sum_pct":1.67,"profit_total_abs":0.0008367,"profit_total":8.367e-07,"profit_total_pct":0.0,"duration_avg":"2:00:00","wins":1,"draws":1,"losses":0},{"key":"ETC/BTC","trades":3,"profit_mean":0.0031163500000000004,"profit_mean_pct":0.31163500000000005,"profit_sum":0.009349050000000001,"profit_sum_pct":0.93,"profit_total_abs":0.00046909,"profit_total":4.6909000000000003e-07,"profit_total_pct":0.0,"duration_avg":"2:17:00","wins":2,"draws":1,"losses":0},{"key":"LTC/BTC","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0},{"key":"XLM/BTC","trades":1,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"7:05:00","wins":0,"draws":1,"losses":0},{"key":"ADA/BTC","trades":4,"profit_mean":-0.015894495,"profit_mean_pct":-1.5894495000000002,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-3.19003e-06,"profit_total_pct":-0.0,"duration_avg":"3:46:00","wins":0,"draws":3,"losses":1},{"key":"TOTAL","trades":11,"profit_mean":0.00020975181818181756,"profit_mean_pct":0.020975181818181757,"profit_sum":0.002307269999999993,"profit_sum_pct":0.23,"profit_total_abs":0.00011576000000000034,"profit_total":1.1576000000000034e-07,"profit_total_pct":0.0,"duration_avg":"3:03:00","wins":4,"draws":6,"losses":1}],"sell_reason_summary":[{"sell_reason":"roi","trades":10,"wins":4,"draws":6,"losses":0,"profit_mean":0.0065885250000000005,"profit_mean_pct":0.66,"profit_sum":0.06588525,"profit_sum_pct":6.59,"profit_total_abs":0.0033057900000000003,"profit_total":0.021961750000000002,"profit_total_pct":2.2},{"sell_reason":"stop_loss","trades":1,"wins":0,"draws":0,"losses":1,"profit_mean":-0.06357798,"profit_mean_pct":-6.36,"profit_sum":-0.06357798,"profit_sum_pct":-6.36,"profit_total_abs":-0.00319003,"profit_total":-0.021192660000000002,"profit_total_pct":-2.12}],"left_open_trades":[{"key":"TOTAL","trades":0,"profit_mean":0.0,"profit_mean_pct":0.0,"profit_sum":0.0,"profit_sum_pct":0.0,"profit_total_abs":0.0,"profit_total":0.0,"profit_total_pct":0.0,"duration_avg":"0:00","wins":0,"draws":0,"losses":0}],"total_trades":11,"total_volume":0.55,"avg_stake_amount":0.05,"profit_mean":0.00020975181818181756,"profit_median":0.0,"profit_total":1.1576000000000034e-07,"profit_total_abs":0.00011576000000000034,"backtest_start":"2018-01-10 07:25:00","backtest_start_ts":1515569100000,"backtest_end":"2018-01-30 04:45:00","backtest_end_ts":1517287500000,"backtest_days":19,"backtest_run_start_ts":1620793107,"backtest_run_end_ts":1620793108,"trades_per_day":0.58,"market_change":0,"pairlist":["ETH/BTC","LTC/BTC","ETC/BTC","XLM/BTC","TRX/BTC","ADA/BTC"],"stake_amount":0.05,"stake_currency":"BTC","stake_currency_decimals":8,"starting_balance":1000,"dry_run_wallet":1000,"final_balance":1000.00011576,"max_open_trades":3,"max_open_trades_setting":3,"timeframe":"5m","timerange":"","enable_protections":false,"strategy_name":"SampleStrategy","stoploss":-0.1,"trailing_stop":false,"trailing_stop_positive":null,"trailing_stop_positive_offset":0.0,"trailing_only_offset_is_reached":false,"use_custom_stoploss":false,"minimal_roi":{"60":0.01,"30":0.02,"0":0.04},"use_sell_signal":true,"sell_profit_only":false,"sell_profit_offset":0.0,"ignore_roi_if_buy_signal":false,"backtest_best_day":0.03986049,"backtest_worst_day":-0.06357798,"backtest_best_day_abs":0.002,"backtest_worst_day_abs":-0.00319003,"winning_days":4,"draw_days":13,"losing_days":1,"wins":4,"losses":1,"draws":6,"holding_avg":"3:03:00","winner_holding_avg":"1:39:00","loser_holding_avg":"3:40:00","max_drawdown":0.06357798,"max_drawdown_abs":0.00319003,"drawdown_start":"2018-01-10 21:15:00","drawdown_start_ts":1515618900000.0,"drawdown_end":"2018-01-13 15:10:00","drawdown_end_ts":1515856200000.0,"max_drawdown_low":-0.00235333,"max_drawdown_high":0.0008367,"csum_min":999.99764667,"csum_max":1000.0008367},"results_explanation":"    11 trades. 4/6/1 Wins/Draws/Losses. Avg profit   0.02%. Median profit   0.00%. Total profit  0.00011576 BTC (   0.00\u03A3%). Avg duration 3:03:00 min.","total_profit":1.1576000000000034e-07,"current_epoch":5,"is_initial_point":true,"is_best":false}

From 24a1d5a96f09cfeb4ad5105bc98ac4c976f0cb30 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 19:04:32 +0200
Subject: [PATCH 366/460] Change default hyperopt-name to be shorter

---
 freqtrade/optimize/hyperopt.py       | 2 +-
 freqtrade/optimize/hyperopt_tools.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index c14b62b9d..422879451 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -87,7 +87,7 @@ class Hyperopt:
         time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
         strategy = str(self.config['strategy'])
         self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' /
-                                   f'strategy_{strategy}_hyperopt_results_{time_now}.fthypt')
+                                   f'strategy_{strategy}_{time_now}.fthypt')
         self.data_pickle_file = (self.config['user_data_dir'] /
                                  'hyperopt_results' / 'hyperopt_tickerdata.pkl')
         self.total_epochs = config.get('epochs', 0)
diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
index 665c393b3..49e70913f 100644
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -182,7 +182,7 @@ class HyperoptTools():
 
     @staticmethod
     def is_best_loss(results, current_best_loss: float) -> bool:
-        return results['loss'] < current_best_loss
+        return bool(results['loss'] < current_best_loss)
 
     @staticmethod
     def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:

From 5e66d37d57927e1275a751ca5a4c78938040b4a0 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 20:07:45 +0200
Subject: [PATCH 367/460] Slightly modify docker instructions for arm64

---
 docs/docker_quickstart.md | 26 ++++++++++++++++----------
 1 file changed, 16 insertions(+), 10 deletions(-)

diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index 51386a4e3..3a85aa885 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -67,21 +67,24 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
         # image: freqtradeorg/freqtrade:develop_pi
         ```
 
-=== "ARM 64 Systenms (Jetson Nano, Mac M1, Raspberry Pi 4 8GB)"
+=== "ARM 64 Systenms (Mac M1, Raspberry Pi 4, Jetson Nano)"
     In case of a Mac M1, make sure that your docker installation is running in native mode
+    Arm64 images are not yet provided via Docker Hub and need to be build locally first.
+    Depending on the device, this may take a few minutes (Apple M1) or multiple hours (Raspberry Pi)
 
     ``` bash
-    mkdir ft_userdata
-    cd ft_userdata/
-
-    # arm64 images are not yet provided via Docker Hub and need to be build locally first. Depending on the device,
-    # this may take a few minutes (Apple M1) or up to two hours (Raspberry Pi)
+    # Clone Freqtrade repository
     git clone  https://github.com/freqtrade/freqtrade.git
-    docker build -f ./freqtrade/docker/Dockerfile.aarch64 -t freqtradeorg/freqtrade:develop_arm64 freqtrade
+    cd freqtrade
+    # Optionally switch to the stable version
+    git checkout stable   
 
-    # Download the docker-compose file from the repository
-    curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
+    # Modify your docker-compose file to enable building and change the image name 
+    # (see the Note Box below for necessary changes)
 
+    # Build image
+    docker-compose build
+    
     # Create user directory structure
     docker-compose run --rm freqtrade create-userdir --userdir user_data
 
@@ -92,7 +95,10 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
     !!! Note "Change your docker Image"
         You have to change the docker image in the docker-compose file for your arm64 build to work properly.
         ``` yml
-        image: freqtradeorg/freqtrade:develop_arm64
+        image: freqtradeorg/freqtrade:custom_arm64
+        build:
+          context: .
+          dockerfile: "./docker/Dockerfile.aarch64"
         ```
 
 The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.

From 1055862bc0d691196c564e405a142ef258b03486 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 12 May 2021 21:15:01 +0200
Subject: [PATCH 368/460] Extract data-load + dump from hyperopt

(Reduces memory-usage as the dataframes go out of scope)
---
 freqtrade/optimize/hyperopt.py | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 422879451..534da34fc 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -345,12 +345,7 @@ class Hyperopt:
     def _set_random_state(self, random_state: Optional[int]) -> int:
         return random_state or random.randint(1, 2**16 - 1)
 
-    def start(self) -> None:
-        self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
-        logger.info(f"Using optimizer random state: {self.random_state}")
-        self.hyperopt_table_header = -1
-        # Initialize spaces ...
-        self.init_spaces()
+    def prepare_hyperopt_data(self) -> None:
         data, timerange = self.backtesting.load_bt_data()
         logger.info("Dataload complete. Calculating indicators")
         preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
@@ -367,6 +362,15 @@ class Hyperopt:
 
         dump(preprocessed, self.data_pickle_file)
 
+    def start(self) -> None:
+        self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
+        logger.info(f"Using optimizer random state: {self.random_state}")
+        self.hyperopt_table_header = -1
+        # Initialize spaces ...
+        self.init_spaces()
+
+        self.prepare_hyperopt_data()
+
         # We don't need exchange instance anymore while running hyperopt
         self.backtesting.exchange.close()
         self.backtesting.exchange._api = None  # type: ignore

From ecee42f5615b87560b54b19049dd0662bb80eec9 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 13 May 2021 20:13:04 +0200
Subject: [PATCH 369/460] Read pickle file in mmap mode

---
 freqtrade/optimize/hyperopt.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 534da34fc..4f8c0ea8b 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -270,7 +270,7 @@ class Hyperopt:
             self.backtesting.strategy.trailing_only_offset_is_reached = \
                 d['trailing_only_offset_is_reached']
 
-        processed = load(self.data_pickle_file)
+        processed = load(self.data_pickle_file, mmap_mode='r+')
 
         bt_results = self.backtesting.backtest(
             processed=processed,

From 09756e300799ff8e6115ba6ac2d358c211d5835d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 14 May 2021 06:36:18 +0200
Subject: [PATCH 370/460] Subplots should always be included in responses

---
 freqtrade/rpc/api_server/api_schemas.py | 2 +-
 freqtrade/rpc/rpc.py                    | 4 +++-
 tests/rpc/test_rpc_apiserver.py         | 8 ++++++++
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py
index e582f6aa8..c324f8828 100644
--- a/freqtrade/rpc/api_server/api_schemas.py
+++ b/freqtrade/rpc/api_server/api_schemas.py
@@ -268,7 +268,7 @@ class DeleteTrade(BaseModel):
 
 class PlotConfig_(BaseModel):
     main_plot: Dict[str, Any]
-    subplots: Optional[Dict[str, Any]]
+    subplots: Dict[str, Any]
 
 
 class PlotConfig(BaseModel):
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index fd97ad7d4..d7564bc65 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -845,5 +845,7 @@ class RPC:
                                               df_analyzed, arrow.Arrow.utcnow().datetime)
 
     def _rpc_plot_config(self) -> Dict[str, Any]:
-
+        if (self._freqtrade.strategy.plot_config and
+                'subplots' not in self._freqtrade.strategy.plot_config):
+            self._freqtrade.strategy.plot_config['subplots'] = {}
         return self._freqtrade.strategy.plot_config
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 69d312e65..fc0dee14b 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -1142,6 +1142,14 @@ def test_api_plot_config(botclient):
     assert_response(rc)
     assert rc.json() == ftbot.strategy.plot_config
     assert isinstance(rc.json()['main_plot'], dict)
+    assert isinstance(rc.json()['subplots'], dict)
+
+    ftbot.strategy.plot_config = {'main_plot': {'sma': {}}}
+    rc = client_get(client, f"{BASE_URI}/plot_config")
+    assert_response(rc)
+
+    assert isinstance(rc.json()['main_plot'], dict)
+    assert isinstance(rc.json()['subplots'], dict)
 
 
 def test_api_strategies(botclient):

From 4bc018a4568c2dbd200875cba61fdd1534004190 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 14 May 2021 07:18:10 +0200
Subject: [PATCH 371/460] Change rate back to "open" for custom_sell

closes #4920
---
 freqtrade/strategy/interface.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 7483abf6d..e2cde52eb 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -573,6 +573,10 @@ class IStrategy(ABC, HyperStrategyMixin):
 
         sell_signal = SellType.NONE
         custom_reason = ''
+        # use provided rate in backtesting, not high/low.
+        current_rate = rate
+        current_profit = trade.calc_profit_ratio(current_rate)
+
         if (ask_strategy.get('sell_profit_only', False)
                 and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
             # sell_profit_only and profit doesn't reach the offset - ignore sell signal

From 09b6923e5042983a348aee55547e830d9f6927f8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 14 May 2021 07:22:51 +0200
Subject: [PATCH 372/460] Use "choose" link for new issues

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 6fd59b62d..ab9597a77 100644
--- a/README.md
+++ b/README.md
@@ -154,7 +154,7 @@ You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/
 If you discover a bug in the bot, please
 [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
 first. If it hasn't been reported, please
-[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
+[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
 ensure you follow the template guide so that our team can assist you as
 quickly as possible.
 
@@ -163,7 +163,7 @@ quickly as possible.
 Have you a great idea to improve the bot you want to share? Please,
 first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
 If it hasn't been requested, please
-[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
+[create a new request](https://github.com/freqtrade/freqtrade/issues/new/choose)
 and ensure you follow the template guide so that it does not get lost
 in the bug reports.
 

From 330fb538a9890100acbdba59a637fde616467e92 Mon Sep 17 00:00:00 2001
From: Rokas Kupstys <19151258+rokups@users.noreply.github.com>
Date: Fri, 14 May 2021 10:43:48 +0300
Subject: [PATCH 373/460] Couple tweaks for docs.

---
 docs/strategy-advanced.md | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index d533d81cd..417218bc3 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -62,7 +62,7 @@ class AwesomeStrategy(IStrategy):
 
         # In dry/live runs trade open date will not match candle open date therefore it must be 
         # rounded.
-        trade_date = timeframe_to_prev_date(trade.open_date_utc)
+        trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
         # Look up trade candle.
         trade_candle = dataframe.loc[dataframe['date'] == trade_date]
         # trade_candle may be None for trades that just opened as it is still incomplete.
@@ -91,8 +91,6 @@ Using custom_sell() signals in place of stoplosses though *is not recommended*.
 An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
 
 ``` python
-from freqtrade.strategy import IStrategy, timeframe_to_prev_date
-
 class AwesomeStrategy(IStrategy):
     def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                     current_profit: float, **kwargs):

From 5e73195b304de01f92ee841468c9d419f877f93a Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 07:01:32 +0200
Subject: [PATCH 374/460] Use linux lineseperator at all times

---
 freqtrade/optimize/hyperopt.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 4f8c0ea8b..3d156cccd 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -5,7 +5,6 @@ This module contains the hyperopt logic
 """
 
 import logging
-import os
 import random
 import warnings
 from datetime import datetime, timezone
@@ -164,7 +163,7 @@ class Hyperopt:
         """
         with self.results_file.open('a') as f:
             rapidjson.dump(epoch, f, default=str, number_mode=rapidjson.NM_NATIVE)
-            f.write(os.linesep)
+            f.write("\n")
 
         self.num_epochs_saved += 1
         logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "

From 0ace35bf3d2dc9071093b830f56944fe5f5e207f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 08:14:50 +0200
Subject: [PATCH 375/460] Fix unreferenced error

---
 freqtrade/rpc/rpc.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index d7564bc65..2c54d743e 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -178,7 +178,7 @@ class RPC:
                     current_rate = trade.close_rate
                 current_profit = trade.calc_profit_ratio(current_rate)
                 current_profit_abs = trade.calc_profit(current_rate)
-
+                current_profit_fiat: Optional[float] = None
                 # Calculate fiat profit
                 if self._fiat_converter:
                     current_profit_fiat = self._fiat_converter.convert_amount(

From 2eac23a15f7c3b1d032fa19bd233da0840fcbf30 Mon Sep 17 00:00:00 2001
From: Brook Miles 
Date: Sat, 15 May 2021 15:38:51 +0900
Subject: [PATCH 376/460] if stoploss price is above the candle high, set it to
 candle open instead.  this can occur if stoploss had previously been reached
 but the sell was prevented by `confirm_trade_exit`

---
 freqtrade/optimize/backtesting.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index e057d8189..689fb9516 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -218,6 +218,12 @@ class Backtesting:
         """
         # Special handling if high or low hit STOP_LOSS or ROI
         if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
+            if trade.stop_loss > sell_row[HIGH_IDX]:
+                # our stoploss was already higher than candle high,
+                # possibly due to a cancelled trade exit.
+                # sell at open price.
+                return sell_row[OPEN_IDX]
+
             # Set close_rate to stoploss
             return trade.stop_loss
         elif sell.sell_type == (SellType.ROI):

From e1447f955cc1ce0b90439175b935eb7a65a33148 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 10:50:00 +0200
Subject: [PATCH 377/460] /locks should always respond, even if there's no
 locks

closes #4942
---
 freqtrade/rpc/telegram.py      | 3 +++
 tests/rpc/test_rpc_telegram.py | 5 +++++
 2 files changed, 8 insertions(+)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index c619559de..8aa96f7b1 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -751,6 +751,9 @@ class Telegram(RPCHandler):
         Returns the currently active locks
         """
         rpc_locks = self._rpc._rpc_locks()
+        if not rpc_locks['locks']:
+            self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
+
         for locks in chunks(rpc_locks['locks'], 25):
             message = tabulate([[
                 lock['id'],
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 6d42a6845..37b5045f8 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -969,6 +969,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
     )
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
     patch_get_signal(freqtradebot, (True, False))
+    telegram._locks(update=update, context=MagicMock())
+    assert msg_mock.call_count == 1
+    assert 'No active locks.' in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
 
     PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
     PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')

From 29fed37df3e92de97cc9a0a75651207fd37c9c3a Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Thu, 13 May 2021 09:47:28 +0300
Subject: [PATCH 378/460] Fix exception when few pairs with no data do not
 result in aborting backtest.

Exception is triggered by backtesting 20210301-20210501 range with BAKE/USDT pair (binance). Pair data starts on 2021-04-30 12:00:00 and after adjusting for startup candles pair dataframe is empty.

Solution: Since there are other pairs with enough data - skip pairs with no data and issue a warning.

Exception:
```
Traceback (most recent call last):
  File "/home/rk/src/freqtrade/freqtrade/main.py", line 37, in main
    return_code = args['func'](args)
  File "/home/rk/src/freqtrade/freqtrade/commands/optimize_commands.py", line 53, in start_backtesting
    backtesting.start()
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 502, in start
    min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 474, in backtest_one_strategy
    results = self.backtest(
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 365, in backtest
    data: Dict = self._get_ohlcv_as_lists(processed)
  File "/home/rk/src/freqtrade/freqtrade/optimize/backtesting.py", line 199, in _get_ohlcv_as_lists
    pair_data.loc[:, 'buy'] = 0  # cleanup from previous run
  File "/home/rk/src/freqtrade/venv/lib/python3.9/site-packages/pandas/core/indexing.py", line 692, in __setitem__
    iloc._setitem_with_indexer(indexer, value, self.name)
  File "/home/rk/src/freqtrade/venv/lib/python3.9/site-packages/pandas/core/indexing.py", line 1587, in _setitem_with_indexer
    raise ValueError(
ValueError: cannot set a frame with no defined index and a scalar
```
---
 freqtrade/optimize/backtesting.py | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index e057d8189..450b88f3b 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -9,7 +9,7 @@ from copy import deepcopy
 from datetime import datetime, timedelta, timezone
 from typing import Any, Dict, List, Optional, Tuple
 
-from pandas import DataFrame, NaT
+from pandas import DataFrame
 
 from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
 from freqtrade.constants import DATETIME_PRINT_FORMAT
@@ -457,13 +457,21 @@ class Backtesting:
         preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
 
         # Trim startup period from analyzed dataframe
-        for pair, df in preprocessed.items():
-            preprocessed[pair] = trim_dataframe(df, timerange,
-                                                startup_candles=self.required_startup)
-        min_date, max_date = history.get_timerange(preprocessed)
-        if min_date is NaT or max_date is NaT:
+        for pair in list(preprocessed):
+            df = preprocessed[pair]
+            df = trim_dataframe(df, timerange, startup_candles=self.required_startup)
+            if len(df) > 0:
+                preprocessed[pair] = df
+            else:
+                logger.warning(f'{pair} has no data left after adjusting for startup candles, '
+                               f'skipping.')
+                del preprocessed[pair]
+
+        if not preprocessed:
             raise OperationalException(
-                "No data left after adjusting for startup candles. ")
+                "No data left after adjusting for startup candles.")
+
+        min_date, max_date = history.get_timerange(preprocessed)
         logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
                     f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
                     f'({(max_date - min_date).days} days).')

From 2d5f465f1b4646f23244692abf0c719cc3ba1cb5 Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Thu, 13 May 2021 11:49:12 +0300
Subject: [PATCH 379/460] Fix protections being loaded multiple times for first
 strategy when backtesting.

---
 freqtrade/optimize/backtesting.py      |  2 --
 freqtrade/optimize/hyperopt.py         |  1 +
 tests/optimize/test_backtest_detail.py |  1 +
 tests/optimize/test_backtesting.py     | 16 ++++++++++++++++
 4 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index 450b88f3b..f05a0d88b 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -115,8 +115,6 @@ class Backtesting:
 
         # Get maximum required startup period
         self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
-        # Load one (first) strategy
-        self._set_strategy(self.strategylist[0])
 
     def __del__(self):
         LoggingMixin.show_output = True
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 5ccf02d01..5ba242bd1 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -79,6 +79,7 @@ class Hyperopt:
             self.custom_hyperopt = HyperOptAuto(self.config)
         else:
             self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
+        self.backtesting._set_strategy(self.backtesting.strategylist[0])
         self.custom_hyperopt.strategy = self.backtesting.strategy
 
         self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py
index 7da7709c6..ca2baaf33 100644
--- a/tests/optimize/test_backtest_detail.py
+++ b/tests/optimize/test_backtest_detail.py
@@ -493,6 +493,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
     patch_exchange(mocker)
     frame = _build_backtest_dataframe(data.data)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = lambda a, m: frame
     backtesting.strategy.advise_sell = lambda a, m: frame
     caplog.set_level(logging.DEBUG)
diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py
index 2ebea564b..03a65b159 100644
--- a/tests/optimize/test_backtesting.py
+++ b/tests/optimize/test_backtesting.py
@@ -83,6 +83,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
     patch_exchange(mocker)
     config['timeframe'] = '1m'
     backtesting = Backtesting(config)
+    backtesting._set_strategy(backtesting.strategylist[0])
 
     data = load_data_test(contour, testdatadir)
     processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
@@ -106,6 +107,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
     data = trim_dictlist(data, -201)
     patch_exchange(mocker)
     backtesting = Backtesting(conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
     min_date, max_date = get_timerange(processed)
     return {
@@ -285,6 +287,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
     patch_exchange(mocker)
     get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     assert backtesting.config == default_conf
     assert backtesting.timeframe == '5m'
     assert callable(backtesting.strategy.ohlcvdata_to_dataframe)
@@ -315,11 +318,13 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
 
     fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     assert backtesting.fee == 0.1234
     assert fee_mock.call_count == 0
 
     default_conf['fee'] = 0.0
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     assert backtesting.fee == 0.0
     assert fee_mock.call_count == 0
 
@@ -330,6 +335,7 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
     data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
                              fill_up_missing=True)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     processed = backtesting.strategy.ohlcvdata_to_dataframe(data)
     assert len(processed['UNITTEST/BTC']) == 102
 
@@ -361,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
     default_conf['timerange'] = '-1510694220'
 
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.bot_loop_start = MagicMock()
     backtesting.start()
     # check the logs, that will contain the backtest result
@@ -393,6 +400,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
     default_conf['timerange'] = '20180101-20180102'
 
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     with pytest.raises(OperationalException, match='No data found. Terminating.'):
         backtesting.start()
 
@@ -465,6 +473,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
     default_conf['stake_amount'] = 'unlimited'
     default_conf['max_open_trades'] = 2
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     pair = 'UNITTEST/BTC'
     row = [
         pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
@@ -508,6 +517,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
     mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
     patch_exchange(mocker)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     pair = 'UNITTEST/BTC'
     timerange = TimeRange('date', None, 1517227800, 0)
     data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
@@ -570,6 +580,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
     mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
     patch_exchange(mocker)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
 
     # Run a backtesting for an exiting 1min timeframe
     timerange = TimeRange.parse_timerange('1510688220-1510700340')
@@ -591,6 +602,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
 def test_processed(default_conf, mocker, testdatadir) -> None:
     patch_exchange(mocker)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
 
     dict_of_tickerrows = load_data_test('raise', testdatadir)
     dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows)
@@ -661,6 +673,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
 
     backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = fun  # Override
     backtesting.strategy.advise_sell = fun  # Override
     result = backtesting.backtest(**backtest_conf)
@@ -676,6 +689,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
 
     backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = fun  # Override
     backtesting.strategy.advise_sell = fun  # Override
     result = backtesting.backtest(**backtest_conf)
@@ -689,6 +703,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
                                         pair='UNITTEST/BTC', datadir=testdatadir)
     default_conf['timeframe'] = '1m'
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = _trend_alternate  # Override
     backtesting.strategy.advise_sell = _trend_alternate  # Override
     result = backtesting.backtest(**backtest_conf)
@@ -731,6 +746,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
     default_conf['timeframe'] = '5m'
 
     backtesting = Backtesting(default_conf)
+    backtesting._set_strategy(backtesting.strategylist[0])
     backtesting.strategy.advise_buy = _trend_alternate_hold  # Override
     backtesting.strategy.advise_sell = _trend_alternate_hold  # Override
 

From 88da1f109b3563a0f3c59d643be9c67de8ed0b89 Mon Sep 17 00:00:00 2001
From: Brook Miles 
Date: Sat, 15 May 2021 20:15:19 +0900
Subject: [PATCH 380/460] fix #4412 download-data does not stop downloading at
 the specified TIMERANGE end date

---
 freqtrade/data/history/history_utils.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py
index 32c7ce7f9..383913f66 100644
--- a/freqtrade/data/history/history_utils.py
+++ b/freqtrade/data/history/history_utils.py
@@ -265,9 +265,13 @@ def _download_trades_history(exchange: Exchange,
     """
     try:
 
-        since = timerange.startts * 1000 if \
-            (timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift(
-                days=-new_pairs_days).float_timestamp) * 1000
+        until = None
+        if (timerange and timerange.starttype == 'date'):
+          since = timerange.startts * 1000
+          if timerange.stoptype == 'date':
+            until = timerange.stopts * 1000
+        else:
+          since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
 
         trades = data_handler.trades_load(pair)
 
@@ -295,6 +299,7 @@ def _download_trades_history(exchange: Exchange,
         # Default since_ms to 30 days if nothing is given
         new_trades = exchange.get_historic_trades(pair=pair,
                                                   since=since,
+                                                  until=until,
                                                   from_id=from_id,
                                                   )
         trades.extend(new_trades[1])

From db17b1a851f749afc0335a8579e0dc1df19c47ce Mon Sep 17 00:00:00 2001
From: Brook Miles 
Date: Sat, 15 May 2021 20:20:36 +0900
Subject: [PATCH 381/460] fix indentation

---
 freqtrade/data/history/history_utils.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py
index 383913f66..86e9f75e6 100644
--- a/freqtrade/data/history/history_utils.py
+++ b/freqtrade/data/history/history_utils.py
@@ -267,11 +267,11 @@ def _download_trades_history(exchange: Exchange,
 
         until = None
         if (timerange and timerange.starttype == 'date'):
-          since = timerange.startts * 1000
-          if timerange.stoptype == 'date':
-            until = timerange.stopts * 1000
+            since = timerange.startts * 1000
+            if timerange.stoptype == 'date':
+                until = timerange.stopts * 1000
         else:
-          since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
+            since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
 
         trades = data_handler.trades_load(pair)
 

From 8e987784984c60a50857efd08e8ae57566da432e Mon Sep 17 00:00:00 2001
From: JoeSchr 
Date: Sat, 15 May 2021 15:21:21 +0200
Subject: [PATCH 382/460] Update installation.md

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

diff --git a/docs/installation.md b/docs/installation.md
index 18f5f4d68..c19965a18 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -269,7 +269,7 @@ git clone https://github.com/freqtrade/freqtrade.git
 cd freqtrade      
 ```
 
-#### Freqtrade instal: Conda Environment
+#### Freqtrade install: Conda Environment
 
 Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
 

From 2ecb42a63971908901facc9823198082c976a3ce Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 15:52:02 +0200
Subject: [PATCH 383/460] Improve rest-api doc config samples

---
 docs/rest-api.md | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/docs/rest-api.md b/docs/rest-api.md
index a0029a44c..b9b2b29be 100644
--- a/docs/rest-api.md
+++ b/docs/rest-api.md
@@ -71,7 +71,10 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
     "api_server": {
         "enabled": true,
         "listen_ip_address": "0.0.0.0",
-        "listen_port": 8080
+        "listen_port": 8080,
+        "username": "Freqtrader",
+        "password": "SuperSecret1!",
+        //...
     },
 ```
 
@@ -106,7 +109,10 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
     "api_server": {
         "enabled": true,
         "listen_ip_address": "0.0.0.0",
-        "listen_port": 8080
+        "listen_port": 8080,
+        "username": "Freqtrader",
+        "password": "SuperSecret1!",
+        //...
     }
 }
 ```

From 6b2a38ccfb9aa3802e6ecc0061d6ee7f09110784 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 19:39:46 +0200
Subject: [PATCH 384/460] Add absolute Profit to apiserver

---
 freqtrade/persistence/models.py         | 8 +++++---
 freqtrade/rpc/api_server/api_schemas.py | 1 +
 tests/rpc/test_rpc_apiserver.py         | 9 ++++++---
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index afd51366a..8d2c9d1d3 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -815,18 +815,20 @@ class Trade(_DECL_BASE, LocalTrade):
         pair_rates = Trade.query.with_entities(
             Trade.pair,
             func.sum(Trade.close_profit).label('profit_sum'),
+            func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
             func.count(Trade.pair).label('count')
         ).filter(Trade.is_open.is_(False))\
             .group_by(Trade.pair) \
-            .order_by(desc('profit_sum')) \
+            .order_by(desc('profit_sum_abs')) \
             .all()
         return [
             {
                 'pair': pair,
-                'profit': rate,
+                'profit': profit,
+                'profit_abs': profit_abs,
                 'count': count
             }
-            for pair, rate, count in pair_rates
+            for pair, profit, profit_abs, count in pair_rates
         ]
 
     @staticmethod
diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py
index c324f8828..4d06d3ecf 100644
--- a/freqtrade/rpc/api_server/api_schemas.py
+++ b/freqtrade/rpc/api_server/api_schemas.py
@@ -57,6 +57,7 @@ class Count(BaseModel):
 class PerformanceEntry(BaseModel):
     pair: str
     profit: float
+    profit_abs: float
     count: int
 
 
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index fc0dee14b..1a66b2e81 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -710,7 +710,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
     assert 'draws' in rc.json()['durations']
 
 
-def test_api_performance(botclient, mocker, ticker, fee):
+def test_api_performance(botclient, fee):
     ftbot, client = botclient
     patch_get_signal(ftbot, (True, False))
 
@@ -728,6 +728,7 @@ def test_api_performance(botclient, mocker, ticker, fee):
 
     )
     trade.close_profit = trade.calc_profit_ratio()
+    trade.close_profit_abs = trade.calc_profit()
     Trade.query.session.add(trade)
 
     trade = Trade(
@@ -743,14 +744,16 @@ def test_api_performance(botclient, mocker, ticker, fee):
         close_rate=0.391
     )
     trade.close_profit = trade.calc_profit_ratio()
+    trade.close_profit_abs = trade.calc_profit()
+
     Trade.query.session.add(trade)
     Trade.query.session.flush()
 
     rc = client_get(client, f"{BASE_URI}/performance")
     assert_response(rc)
     assert len(rc.json()) == 2
-    assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61},
-                         {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}]
+    assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279},
+                         {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
 
 
 def test_api_status(botclient, mocker, ticker, fee, markets):

From 2d7735ba048f1eb18a2301e9824432633721d32e Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 15 May 2021 19:49:21 +0200
Subject: [PATCH 385/460] Update telegram to sort performance by absolute
 performance

---
 docs/telegram-usage.md         | 10 +++++-----
 freqtrade/rpc/telegram.py      |  7 +++++--
 tests/rpc/test_rpc_telegram.py |  2 +-
 3 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index 824cb17c7..07f5fe7dd 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -262,11 +262,11 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
 
 Return the performance of each crypto-currency the bot has sold.
 > Performance:
-> 1. `RCN/BTC 57.77%`  
-> 2. `PAY/BTC 56.91%`  
-> 3. `VIB/BTC 47.07%`  
-> 4. `SALT/BTC 30.24%`  
-> 5. `STORJ/BTC 27.24%`  
+> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
+> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
+> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
+> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
+> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
 > ...  
 
 ### /balance
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 8aa96f7b1..2e288ee33 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -711,8 +711,11 @@ class Telegram(RPCHandler):
             trades = self._rpc._rpc_performance()
             output = "Performance:\n"
             for i, trade in enumerate(trades):
-                stat_line = (f"{i+1}.\t {trade['pair']}\t{trade['profit']:.2f}% "
-                             f"({trade['count']})\n")
+                stat_line = (
+                    f"{i+1}.\t {trade['pair']}\t"
+                    f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
+                    f"({trade['profit']:.2f}%) "
+                    f"({trade['count']})\n")
 
                 if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
                     self._send_msg(output, parse_mode=ParseMode.HTML)
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 37b5045f8..6008ede66 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -929,7 +929,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
     telegram._performance(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert 'Performance' in msg_mock.call_args_list[0][0][0]
-    assert 'ETH/BTC\t6.20% (1)' in msg_mock.call_args_list[0][0][0]
+    assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0]
 
 
 def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:

From 0b1dd0d20382f9f287dba6e28f16515edb70b02b Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 09:08:13 +0200
Subject: [PATCH 386/460] Use correct order_id for ftx

closes #4511
---
 freqtrade/exchange/exchange.py |  3 +++
 freqtrade/exchange/ftx.py      |  6 ++++++
 freqtrade/freqtradebot.py      |  4 ++--
 tests/exchange/test_ftx.py     | 23 +++++++++++++++++++++++
 4 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 5696724b9..474eabcf3 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1239,6 +1239,9 @@ class Exchange:
         except ccxt.BaseError as e:
             raise OperationalException(e) from e
 
+    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
+        return order['id']
+
     @retrier
     def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
                 price: float = 1, taker_or_maker: str = 'maker') -> float:
diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py
index fdfd5a674..9009e9492 100644
--- a/freqtrade/exchange/ftx.py
+++ b/freqtrade/exchange/ftx.py
@@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
                                   OperationalException, TemporaryError)
 from freqtrade.exchange import Exchange
 from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
+from freqtrade.misc import safe_value_fallback2
 
 
 logger = logging.getLogger(__name__)
@@ -135,3 +136,8 @@ class Ftx(Exchange):
                 f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
         except ccxt.BaseError as e:
             raise OperationalException(e) from e
+
+    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
+        if order['type'] == 'stop':
+            return safe_value_fallback2(order['info'], order, 'orderId', 'id')
+        return order['id']
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index b3379a462..f451741a2 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1427,8 +1427,8 @@ class FreqtradeBot(LoggingMixin):
         """
         fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
         """
-        trades = self.exchange.get_trades_for_order(order['id'], trade.pair,
-                                                    trade.open_date)
+        trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
+                                                    trade.pair, trade.open_date)
 
         if len(trades) == 0:
             logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py
index 494d86e56..63d99acdf 100644
--- a/tests/exchange/test_ftx.py
+++ b/tests/exchange/test_ftx.py
@@ -157,3 +157,26 @@ def test_fetch_stoploss_order(default_conf, mocker):
                            'fetch_stoploss_order', 'fetch_orders',
                            retries=API_FETCH_ORDER_RETRY_COUNT + 1,
                            order_id='_', pair='TKN/BTC')
+
+
+def test_get_order_id(mocker, default_conf):
+    exchange = get_patched_exchange(mocker, default_conf, id='ftx')
+    order = {
+        'type': STOPLOSS_ORDERTYPE,
+        'price': 1500,
+        'id': '1111',
+        'info': {
+            'orderId': '1234'
+        }
+    }
+    assert exchange.get_order_id_conditional(order) == '1234'
+
+    order = {
+        'type': 'limit',
+        'price': 1500,
+        'id': '1111',
+        'info': {
+            'orderId': '1234'
+        }
+    }
+    assert exchange.get_order_id_conditional(order) == '1111'

From 380754b8ab83187015a5d929e30c50150063fd53 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 13:16:17 +0200
Subject: [PATCH 387/460] Fix typos in docstrings

---
 freqtrade/freqtradebot.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index f451741a2..4013072c9 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -630,7 +630,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_buy(self, trade: Trade, order_type: str) -> None:
         """
-        Sends rpc notification when a buy occured.
+        Sends rpc notification when a buy occurred.
         """
         msg = {
             'trade_id': trade.id,
@@ -652,7 +652,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
         """
-        Sends rpc notification when a buy cancel occured.
+        Sends rpc notification when a buy cancel occurred.
         """
         current_rate = self.get_buy_rate(trade.pair, False)
 
@@ -713,7 +713,7 @@ class FreqtradeBot(LoggingMixin):
             except DependencyException as exception:
                 logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
 
-        # Updating wallets if any trade occured
+        # Updating wallets if any trade occurred
         if trades_closed:
             self.wallets.update()
 
@@ -932,7 +932,7 @@ class FreqtradeBot(LoggingMixin):
         :return: None
         """
         if self.exchange.stoploss_adjust(trade.stop_loss, order):
-            # we check if the update is neccesary
+            # we check if the update is necessary
             update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
             if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
                 # cancelling the current stoploss on exchange first
@@ -981,7 +981,7 @@ class FreqtradeBot(LoggingMixin):
 
     def check_handle_timedout(self) -> None:
         """
-        Check if any orders are timed out and cancel if neccessary
+        Check if any orders are timed out and cancel if necessary
         :param timeoutvalue: Number of minutes until order is considered timed out
         :return: None
         """
@@ -1230,7 +1230,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
         """
-        Sends rpc notification when a sell occured.
+        Sends rpc notification when a sell occurred.
         """
         profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
         profit_trade = trade.calc_profit(rate=profit_rate)
@@ -1271,7 +1271,7 @@ class FreqtradeBot(LoggingMixin):
 
     def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
         """
-        Sends rpc notification when a sell cancel occured.
+        Sends rpc notification when a sell cancel occurred.
         """
         if trade.sell_order_status == reason:
             return

From 6f389764701c23ca00f350e3f696bf9e84212fbd Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 14:15:24 +0200
Subject: [PATCH 388/460] Introduce cancel_stoploss_with_result

---
 freqtrade/exchange/exchange.py | 21 +++++++++++++++++++++
 freqtrade/freqtradebot.py      |  7 +++++--
 tests/test_freqtradebot.py     |  3 ++-
 tests/test_integration.py      |  2 +-
 4 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 474eabcf3..c0674cd05 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1120,6 +1120,27 @@ class Exchange:
 
         return order
 
+    def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
+        """
+        Cancel stoploss order returning a result.
+        Creates a fake result if cancel order returns a non-usable result
+        and fetch_order does not work (certain exchanges don't return cancelled orders)
+        :param order_id: stoploss-order-id to cancel
+        :param pair: Pair corresponding to order_id
+        :param amount: Amount to use for fake response
+        :return: Result from either cancel_order if usable, or fetch_order
+        """
+        corder = self.cancel_stoploss_order(order_id, pair)
+        if self.is_cancel_order_result_suitable(corder):
+            return corder
+        try:
+            order = self.fetch_stoploss_order(order_id, pair)
+        except InvalidOrderException:
+            logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
+            order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
+
+        return order
+
     @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
     def fetch_order(self, order_id: str, pair: str) -> Dict:
         if self._config['dry_run']:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 4013072c9..9bfc343f6 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -939,7 +939,8 @@ class FreqtradeBot(LoggingMixin):
                 logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
                             f"(orderid:{order['id']}) in order to add another one ...")
                 try:
-                    co = self.exchange.cancel_stoploss_order(order['id'], trade.pair)
+                    co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
+                                                                         trade.amount)
                     trade.update_order(co)
                 except InvalidOrderException:
                     logger.exception(f"Could not cancel stoploss order {order['id']} "
@@ -1172,7 +1173,9 @@ class FreqtradeBot(LoggingMixin):
         # First cancelling stoploss on exchange ...
         if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
             try:
-                self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
+                co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id,
+                                                                     trade.pair, trade.amount)
+                trade.update_order(co)
             except InvalidOrderException:
                 logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 785e866ae..df0715111 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1371,7 +1371,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
     }
     mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order',
                  side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
+    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
+                 return_value=stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
 
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 217910961..33b3e1140 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -63,7 +63,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
         fetch_stoploss_order=stoploss_order_mock,
-        cancel_stoploss_order=cancel_order_mock,
+        cancel_stoploss_order_with_result=cancel_order_mock,
     )
 
     mocker.patch.multiple(

From 8f8d5dbff5175a8c411d90fb39d255733afc49a4 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 14:31:53 +0200
Subject: [PATCH 389/460] Add tests for sl_order_with_result

---
 tests/exchange/test_exchange.py | 41 +++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 573f41bda..54ffabd4f 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -2084,6 +2084,47 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
                            order_id='_', pair='TKN/BTC')
 
 
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
+    default_conf['dry_run'] = False
+    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123})
+    mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', return_value={'for': 123})
+    exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                 return_value={'fee': {}, 'status': 'canceled', 'amount': 1234})
+    mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
+                 return_value={'fee': {}, 'status': 'canceled', 'amount': 1234})
+    co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
+    assert co == {'fee': {}, 'status': 'canceled', 'amount': 1234}
+
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                 return_value='canceled')
+    mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
+                 return_value='canceled')
+    # Fall back to fetch_stoploss_order
+    co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
+    assert co == {'for': 123}
+
+    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
+                 side_effect=InvalidOrderException(""))
+    mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order',
+                 side_effect=InvalidOrderException(""))
+
+    co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
+    assert co['amount'] == 555
+    assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
+
+    with pytest.raises(InvalidOrderException):
+        mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                     side_effect=InvalidOrderException("Did not find order"))
+        mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order',
+                     side_effect=InvalidOrderException("Did not find order"))
+        exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+        exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
+
+
+
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
 def test_fetch_order(default_conf, mocker, exchange_name):
     default_conf['dry_run'] = True

From c9ac67e985914bb3700c3401eeb95eb1f437cba0 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 14:50:25 +0200
Subject: [PATCH 390/460] Fix some typos

---
 freqtrade/exchange/exchange.py  | 4 ++--
 freqtrade/freqtradebot.py       | 8 ++++----
 tests/exchange/test_exchange.py | 1 -
 3 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index c0674cd05..93d8f7584 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -466,7 +466,7 @@ class Exchange:
     def amount_to_precision(self, pair: str, amount: float) -> float:
         '''
         Returns the amount to buy or sell to a precision the Exchange accepts
-        Reimplementation of ccxt internal methods - ensuring we can test the result is correct
+        Re-implementation of ccxt internal methods - ensuring we can test the result is correct
         based on our definitions.
         '''
         if self.markets[pair]['precision']['amount']:
@@ -480,7 +480,7 @@ class Exchange:
     def price_to_precision(self, pair: str, price: float) -> float:
         '''
         Returns the price rounded up to the precision the Exchange accepts.
-        Partial Reimplementation of ccxt internal method decimal_to_precision(),
+        Partial Re-implementation of ccxt internal method decimal_to_precision(),
         which does not support rounding up
         TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
         align with amount_to_precision().
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 9bfc343f6..0bef29dd4 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -267,7 +267,7 @@ class FreqtradeBot(LoggingMixin):
     def update_closed_trades_without_assigned_fees(self):
         """
         Update closed trades without close fees assigned.
-        Only acts when Orders are in the database, otherwise the last orderid is unknown.
+        Only acts when Orders are in the database, otherwise the last order-id is unknown.
         """
         if self.config['dry_run']:
             # Updating open orders in dry-run does not make sense and will fail.
@@ -1223,7 +1223,7 @@ class FreqtradeBot(LoggingMixin):
             self.update_trade_state(trade, trade.open_order_id, order)
         Trade.query.session.flush()
 
-        # Lock pair for one candle to prevent immediate rebuys
+        # Lock pair for one candle to prevent immediate re-buys
         self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
                                 reason='Auto lock')
 
@@ -1327,7 +1327,7 @@ class FreqtradeBot(LoggingMixin):
         Handles closing both buy and sell orders.
         :param trade: Trade object of the trade we're analyzing
         :param order_id: Order-id of the order we're analyzing
-        :param action_order: Already aquired order object
+        :param action_order: Already acquired order object
         :return: True if order has been cancelled without being filled partially, False otherwise
         """
         if not order_id:
@@ -1397,7 +1397,7 @@ class FreqtradeBot(LoggingMixin):
     def get_real_amount(self, trade: Trade, order: Dict) -> float:
         """
         Detect and update trade fee.
-        Calls trade.update_fee() uppon correct detection.
+        Calls trade.update_fee() upon correct detection.
         Returns modified amount if the fee was taken from the destination currency.
         Necessary for exchanges which charge fees in base currency (e.g. binance)
         :return: identical (or new) amount for the trade
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 54ffabd4f..b6b395802 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -2124,7 +2124,6 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
         exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
 
 
-
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
 def test_fetch_order(default_conf, mocker, exchange_name):
     default_conf['dry_run'] = True

From 0d50e995634dac4e6e3d2a3fbbbcc776671a3618 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 19:35:30 +0200
Subject: [PATCH 391/460] Fix Agefilter checking for > instead of >=

---
 freqtrade/plugins/pairlist/AgeFilter.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py
index 837e99f2a..7c18460cb 100644
--- a/freqtrade/plugins/pairlist/AgeFilter.py
+++ b/freqtrade/plugins/pairlist/AgeFilter.py
@@ -86,7 +86,7 @@ class AgeFilter(IPairList):
             return True
 
         if daily_candles is not None:
-            if len(daily_candles) > self._min_days_listed:
+            if len(daily_candles) >= self._min_days_listed:
                 # We have fetched at least the minimum required number of daily candles
                 # Add to cache, store the time we last checked this symbol
                 self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000

From 37b71b8cfd49500b794cd75fa56e5c9019a6c3c9 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 16 May 2021 19:55:13 +0200
Subject: [PATCH 392/460] Fix PerformanceFilter failing in test-pairlist mode

---
 freqtrade/plugins/pairlist/PerformanceFilter.py |  7 ++++++-
 tests/plugins/test_pairlist.py                  | 15 ++++++++++++++-
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py
index 73a9436fa..bf474cb21 100644
--- a/freqtrade/plugins/pairlist/PerformanceFilter.py
+++ b/freqtrade/plugins/pairlist/PerformanceFilter.py
@@ -39,7 +39,12 @@ class PerformanceFilter(IPairList):
         :return: new allowlist
         """
         # Get the trading performance for pairs from database
-        performance = pd.DataFrame(Trade.get_overall_performance())
+        try:
+            performance = pd.DataFrame(Trade.get_overall_performance())
+        except AttributeError:
+            # Performancefilter does not work in backtesting.
+            self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
+            return pairlist
 
         # Skip performance-based sorting if no performance data is available
         if len(performance) == 0:
diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py
index 8b060c287..8347687da 100644
--- a/tests/plugins/test_pairlist.py
+++ b/tests/plugins/test_pairlist.py
@@ -7,10 +7,11 @@ import pytest
 
 from freqtrade.constants import AVAILABLE_PAIRLISTS
 from freqtrade.exceptions import OperationalException
+from freqtrade.persistence import Trade
 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
 from freqtrade.plugins.pairlistmanager import PairListManager
 from freqtrade.resolvers import PairListResolver
-from tests.conftest import get_patched_freqtradebot, log_has, log_has_re
+from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re
 
 
 @pytest.fixture(scope="function")
@@ -512,6 +513,18 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
         PairListManager(MagicMock, whitelist_conf)
 
 
+def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
+    whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
+    if hasattr(Trade, 'query'):
+        del Trade.query
+    mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
+    exchange = get_patched_exchange(mocker, whitelist_conf)
+    pm = PairListManager(exchange, whitelist_conf)
+    pm.refresh_pairlist()
+
+    assert log_has("PerformanceFilter is not available in this mode.", caplog)
+
+
 def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
     default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
 

From b0f854af9507db9a55c6922aab339c53bc4f5b97 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:02 +0000
Subject: [PATCH 393/460] Bump ccxt from 1.49.73 to 1.50.6

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.49.73 to 1.50.6.
- [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.49.73...1.50.6)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..54f308df5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 numpy==1.20.2
 pandas==1.2.4
 
-ccxt==1.49.73
+ccxt==1.50.6
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0

From 78c77cca737bd3cb468aecfecae3db53afff19ef Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:11 +0000
Subject: [PATCH 394/460] Bump pytest-cov from 2.11.1 to 2.12.0

Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.1 to 2.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.11.1...v2.12.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 be7ee6857..e85120b3c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -10,7 +10,7 @@ flake8-tidy-imports==4.2.1
 mypy==0.812
 pytest==6.2.4
 pytest-asyncio==0.15.1
-pytest-cov==2.11.1
+pytest-cov==2.12.0
 pytest-mock==3.6.1
 pytest-random-order==1.0.4
 isort==5.8.0

From 439ef197bc65e055208780a7bd088ff8814447d8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:18 +0000
Subject: [PATCH 395/460] Bump flake8-tidy-imports from 4.2.1 to 4.3.0

Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases)
- [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/main/HISTORY.rst)
- [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.2.1...4.3.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 be7ee6857..1fb8e6e7d 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -6,7 +6,7 @@
 coveralls==3.0.1
 flake8==3.9.2
 flake8-type-annotations==0.1.0
-flake8-tidy-imports==4.2.1
+flake8-tidy-imports==4.3.0
 mypy==0.812
 pytest==6.2.4
 pytest-asyncio==0.15.1

From 976a026d3b027a0bb5a0bc106a4a5db4f14702ac Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:31 +0000
Subject: [PATCH 396/460] Bump sqlalchemy from 1.4.14 to 1.4.15

Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.14 to 1.4.15.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..f35138c30 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@ ccxt==1.49.73
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==3.4.7
 aiohttp==3.7.4.post0
-SQLAlchemy==1.4.14
+SQLAlchemy==1.4.15
 python-telegram-bot==13.5
 arrow==1.1.0
 cachetools==4.2.2

From 8143e6385307087d2b1d2a5facb6e9d4c320cbc3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:36 +0000
Subject: [PATCH 397/460] Bump aiofiles from 0.6.0 to 0.7.0

Bumps [aiofiles](https://github.com/Tinche/aiofiles) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/Tinche/aiofiles/releases)
- [Commits](https://github.com/Tinche/aiofiles/compare/v0.6.0...v0.7.0)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..2987db0b0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,7 +34,7 @@ sdnotify==0.3.2
 fastapi==0.64.0
 uvicorn==0.13.4
 pyjwt==2.1.0
-aiofiles==0.6.0
+aiofiles==0.7.0
 
 # Support for colorized terminal output
 colorama==0.4.4

From c0b61282fbe4fb03217d7243af1487250324b045 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 05:21:52 +0000
Subject: [PATCH 398/460] Bump jinja2 from 2.11.3 to 3.0.0

Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.3 to 3.0.0.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.11.3...3.0.0)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 3c50fa586..a3f818228 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,7 +17,7 @@ TA-Lib==0.4.19
 technical==1.3.0
 tabulate==0.8.9
 pycoingecko==2.0.0
-jinja2==2.11.3
+jinja2==3.0.0
 tables==3.6.1
 blosc==1.10.2
 

From 40ae21f3a8871bda3978d4a40934bff52e082f98 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 May 2021 07:59:36 +0000
Subject: [PATCH 399/460] Bump numpy from 1.20.2 to 1.20.3

Bumps [numpy](https://github.com/numpy/numpy) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.20.2...v1.20.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 a9dce82c3..bd8c5e65d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-numpy==1.20.2
+numpy==1.20.3
 pandas==1.2.4
 
 ccxt==1.50.6

From 10ef0f54ac7847e34fc4f096a50b63144bafb42e Mon Sep 17 00:00:00 2001
From: Eugene Schava 
Date: Mon, 17 May 2021 11:12:11 +0300
Subject: [PATCH 400/460] Total row for telegram "/status table" command

---
 freqtrade/rpc/telegram.py | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 2e288ee33..157a2b9a5 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -5,6 +5,7 @@ This module manage Telegram communication
 """
 import json
 import logging
+import re
 from datetime import timedelta
 from html import escape
 from itertools import chain
@@ -354,19 +355,33 @@ class Telegram(RPCHandler):
         :return: None
         """
         try:
+            fiat_currency = self._config.get('fiat_display_currency', '')
             statlist, head = self._rpc._rpc_status_table(
-                self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
+                self._config['stake_currency'], fiat_currency)
 
+            show_total = fiat_currency != ''
+            total_sum = 0
+            if show_total:
+                total_sum = sum(map(lambda m: float(m[1]) if m else 0, map(lambda trade: re.compile(".*\((-?\d*\.\d*)\)").match(trade[-1]), statlist)))
             max_trades_per_msg = 50
             """
             Calculate the number of messages of 50 trades per message
             0.99 is used to make sure that there are no extra (empty) messages
             As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
             """
-            for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
+            messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1)
+            for i in range(0, messages_count):
+                if show_total and i == messages_count - 1:
+                    # append total line
+                    trades.append(["Total", "", "", f"{total_sum:.2f} {fiat_currency}"])
+
                 message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
                                    headers=head,
                                    tablefmt='simple')
+                if show_total and i == messages_count - 1:
+                    # insert separators line between Total
+                    lines = message.split("\n")
+                    message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
                 self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 860a4d239065132760947d0da7e507e462292943 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Mon, 17 May 2021 11:40:57 +0200 Subject: [PATCH 401/460] update doc to reflect better empty dataframe check --- docs/strategy-advanced.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 417218bc3..f5f706cd6 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -65,9 +65,10 @@ class AwesomeStrategy(IStrategy): trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) # Look up trade candle. trade_candle = dataframe.loc[dataframe['date'] == trade_date] - # trade_candle may be None for trades that just opened as it is still incomplete. - if trade_candle is not None: + # trade_candle may be empty for trades that just opened as it is still incomplete. + if trade_candle.empty: # <...> + trade_candle = trade_candle.squeeze() ``` !!! Warning "Using .iloc[-1]" From 0abb9cfe2854ba02d02a048e74ee09667f5846a2 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 12:41:44 +0300 Subject: [PATCH 402/460] Total row for telegram "/status table" command --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 157a2b9a5..d4a79e860 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -371,11 +371,12 @@ class Telegram(RPCHandler): """ messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1) for i in range(0, messages_count): + trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg] if show_total and i == messages_count - 1: # append total line trades.append(["Total", "", "", f"{total_sum:.2f} {fiat_currency}"]) - message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], + message = tabulate(trades, headers=head, tablefmt='simple') if show_total and i == messages_count - 1: From d7479fda1fbc12bd1e48c40e1f4d0a4e0d79f22f Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 12:53:57 +0300 Subject: [PATCH 403/460] Total row for telegram "/status table" command fix compiler warnings --- freqtrade/rpc/telegram.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d4a79e860..11b84de19 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -362,7 +362,11 @@ class Telegram(RPCHandler): show_total = fiat_currency != '' total_sum = 0 if show_total: - total_sum = sum(map(lambda m: float(m[1]) if m else 0, map(lambda trade: re.compile(".*\((-?\d*\.\d*)\)").match(trade[-1]), statlist))) + total_sum = sum( + map(lambda m: float(m[1]) if m else 0, + map(lambda trade: re.compile(".*\\((-?\\d*\\.\\d*)\\)").match(trade[-1]), + statlist)) + ) max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message From 915ff7e1bf1887795b4a561d737c9b2f56b2eba8 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 13:03:20 +0300 Subject: [PATCH 404/460] Total row for telegram "/status table" command fix mypy warnings --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 11b84de19..6a12488d1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -363,7 +363,7 @@ class Telegram(RPCHandler): total_sum = 0 if show_total: total_sum = sum( - map(lambda m: float(m[1]) if m else 0, + map(lambda m: float(m[1]) if m else 0.0, map(lambda trade: re.compile(".*\\((-?\\d*\\.\\d*)\\)").match(trade[-1]), statlist)) ) From 196fde44e070274dca17b2c219c790100d8ec691 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 14:45:54 +0300 Subject: [PATCH 405/460] Total row for telegram "/status table" command work around mypy warning --- freqtrade/rpc/telegram.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6a12488d1..1e0fad8fd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -360,13 +360,14 @@ class Telegram(RPCHandler): self._config['stake_currency'], fiat_currency) show_total = fiat_currency != '' - total_sum = 0 + total_sum = 0.0 if show_total: - total_sum = sum( - map(lambda m: float(m[1]) if m else 0.0, - map(lambda trade: re.compile(".*\\((-?\\d*\\.\\d*)\\)").match(trade[-1]), - statlist)) - ) + r = re.compile(".*\\((-?\\d*\\.\\d*)\\)") + for trade in statlist: + m = r.match(trade[-1]) + if m is not None: + total_sum += float(m[1]) + max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message From cb50298bfec480ca9df5fa437033deeeac357c7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 12:05:13 +0000 Subject: [PATCH 406/460] Bump fastapi from 0.64.0 to 0.65.1 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.64.0 to 0.65.1. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.64.0...0.65.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 762d2f491..f55cc0bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ python-rapidjson==1.0 sdnotify==0.3.2 # API Server -fastapi==0.64.0 +fastapi==0.65.1 uvicorn==0.13.4 pyjwt==2.1.0 aiofiles==0.7.0 From 3ad8fa2f38cdd28322b2839e53674a17b66a610e Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 15:59:03 +0300 Subject: [PATCH 407/460] Total row for telegram "/status table" command moved sum calculation to API --- freqtrade/rpc/rpc.py | 6 ++++-- freqtrade/rpc/telegram.py | 16 ++++------------ tests/rpc/test_rpc.py | 9 ++++++--- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2c54d743e..9b0ef2ad0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -220,12 +220,13 @@ class RPC: return results def _rpc_status_table(self, stake_currency: str, - fiat_display_currency: str) -> Tuple[List, List]: + fiat_display_currency: str) -> Tuple[List, List, float]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: trades_list = [] + fiat_profit_sum = NAN for trade in trades: # calculate profit and send message to user try: @@ -243,6 +244,7 @@ class RPC: ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" + fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit trades_list.append([ trade.id, trade.pair + ('*' if (trade.open_order_id is not None @@ -256,7 +258,7 @@ class RPC: profitcol += " (" + fiat_display_currency + ")" columns = ['ID', 'Pair', 'Since', profitcol] - return trades_list, columns + return trades_list, columns, fiat_profit_sum def _rpc_daily_profit( self, timescale: int, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1e0fad8fd..bdb1590a5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,10 +5,10 @@ This module manage Telegram communication """ import json import logging -import re from datetime import timedelta from html import escape from itertools import chain +from math import isnan from typing import Any, Callable, Dict, List, Union import arrow @@ -356,18 +356,10 @@ class Telegram(RPCHandler): """ try: fiat_currency = self._config.get('fiat_display_currency', '') - statlist, head = self._rpc._rpc_status_table( + statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) - show_total = fiat_currency != '' - total_sum = 0.0 - if show_total: - r = re.compile(".*\\((-?\\d*\\.\\d*)\\)") - for trade in statlist: - m = r.match(trade[-1]) - if m is not None: - total_sum += float(m[1]) - + show_total = not isnan(fiat_profit_sum) and len(statlist) > 1 max_trades_per_msg = 50 """ Calculate the number of messages of 50 trades per message @@ -379,7 +371,7 @@ class Telegram(RPCHandler): trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg] if show_total and i == messages_count - 1: # append total line - trades.append(["Total", "", "", f"{total_sum:.2f} {fiat_currency}"]) + trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"]) message = tabulate(trades, headers=head, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6d31e7635..0ae214615 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -199,28 +199,31 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41%' == result[0][3] + assert isnan(fiat_profit_sum) # Test with fiatconvert rpc._fiat_converter = CryptoToFiatConverter() - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41% (-0.06)' == result[0][3] + assert -0.06 == fiat_profit_sum mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) - result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') + result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert 'nan%' == result[0][3] + assert isnan(fiat_profit_sum) def test_rpc_daily_profit(default_conf, update, ticker, fee, From 459fae6d80608f45b5f34dcfb61a0de388035d5f Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Mon, 17 May 2021 16:22:48 +0300 Subject: [PATCH 408/460] Total row for telegram "/status table" command fixes --- freqtrade/rpc/rpc.py | 3 ++- tests/rpc/test_rpc.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b0ef2ad0..3f26619a9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -244,7 +244,8 @@ class RPC: ) if fiat_profit and not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" - fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit + fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ + else fiat_profit_sum + fiat_profit trades_list.append([ trade.id, trade.pair + ('*' if (trade.open_order_id is not None diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0ae214615..b005fb105 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -215,7 +215,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41% (-0.06)' == result[0][3] - assert -0.06 == fiat_profit_sum + assert '-0.06' == f'{fiat_profit_sum:.2f}' mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) From 30063963982ec6ea5e56ad0fbb3783f43d4f1713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 20:34:02 +0200 Subject: [PATCH 409/460] Fix docstring typo --- freqtrade/plugins/pairlist/AgeFilter.py | 2 +- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlist/PrecisionFilter.py | 2 +- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- freqtrade/plugins/pairlist/SpreadFilter.py | 2 +- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- freqtrade/plugins/pairlist/rangestabilityfilter.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 7c18460cb..8f623b062 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -78,7 +78,7 @@ class AgeFilter(IPairList): """ Validate age for the ticker :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 3e6252fc4..01eec7e90 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(LoggingMixin, ABC): filter_pairlist() method. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ raise NotImplementedError() diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index 519337f29..a3c262e8c 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -48,7 +48,7 @@ class PrecisionFilter(IPairList): Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very low value pairs. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ stop_price = ticker['ask'] * self._stoploss diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index a0579b196..7f2fa4444 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -61,7 +61,7 @@ class PriceFilter(IPairList): """ Check if if one price-step (pip) is > than a certain barrier. :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ if ticker.get('last', None) is None or ticker.get('last') == 0: diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 9fa211750..1b152774b 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -40,7 +40,7 @@ class SpreadFilter(IPairList): """ Validate spread for the ticker :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ if 'bid' in ticker and 'ask' in ticker and ticker['ask']: diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 400b1577d..bc617a1db 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -90,7 +90,7 @@ class VolatilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 6565e92c1..8be61166b 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -83,7 +83,7 @@ class RangeStabilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() + :param ticker: ticker dict as returned from ccxt.fetch_tickers() :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache From 369f19df6be9fa0141953a2ababa39e6c7447862 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 06:47:49 +0200 Subject: [PATCH 410/460] Add valuefilter to Pricefilters --- docs/includes/pairlists.md | 6 ++++ freqtrade/plugins/pairlist/IPairList.py | 6 ++-- freqtrade/plugins/pairlist/PriceFilter.py | 36 +++++++++++++++++++++-- tests/plugins/test_pairlist.py | 8 +++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 20883c825..ce0cc6e57 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -112,6 +112,7 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr * `min_price` * `max_price` +* `max_value` * `low_price_ratio` The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. @@ -120,6 +121,11 @@ This option is disabled by default, and will only apply if set to > 0. The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. This option is disabled by default, and will only apply if set to > 0. +The `max_value` setting removes pairs where the minimum value change is above a specified value. +This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption. +As a result of the above, you can only buy for 20$, or 40$ - but not for 25$. +On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit. + The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to > 0. diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 01eec7e90..74348b1a7 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -7,7 +7,7 @@ from copy import deepcopy from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.exchange import market_is_active +from freqtrade.exchange import Exchange, market_is_active from freqtrade.mixins import LoggingMixin @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class IPairList(LoggingMixin, ABC): - def __init__(self, exchange, pairlistmanager, + def __init__(self, exchange: Exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: """ @@ -28,7 +28,7 @@ class IPairList(LoggingMixin, ABC): """ self._enabled = True - self._exchange = exchange + self._exchange: Exchange = exchange self._pairlistmanager = pairlistmanager self._config = config self._pairlistconfig = pairlistconfig diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 7f2fa4444..f25ab8fd0 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -27,9 +27,13 @@ class PriceFilter(IPairList): self._max_price = pairlistconfig.get('max_price', 0) if self._max_price < 0: raise OperationalException("PriceFilter requires max_price to be >= 0") + self._max_value = pairlistconfig.get('max_value', 0) + if self._max_value < 0: + raise OperationalException("PriceFilter requires max_value to be >= 0") self._enabled = ((self._low_price_ratio > 0) or (self._min_price > 0) or - (self._max_price > 0)) + (self._max_price > 0) or + (self._max_value)) @property def needstickers(self) -> bool: @@ -51,6 +55,8 @@ class PriceFilter(IPairList): active_price_filters.append(f"below {self._min_price:.8f}") if self._max_price != 0: active_price_filters.append(f"above {self._max_price:.8f}") + if self._max_value != 0: + active_price_filters.append(f"Value above {self._max_value:.8f}") if len(active_price_filters): return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." @@ -79,6 +85,32 @@ class PriceFilter(IPairList): f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False + # Perform low_amount check + if self._max_value != 0: + price = ticker['last'] + market = self._exchange.markets[pair] + limits = market['limits'] + if ('amount' in limits and 'min' in limits['amount'] + and limits['amount']['min'] is not None): + min_amount = limits['amount']['min'] + min_precision = market['precision']['amount'] + + min_value = min_amount * price + if self._exchange.precisionMode == 4: + # tick size + next_value = (min_amount + min_precision) * price + else: + # Decimal places + min_precision = pow(0.1, min_precision) + next_value = (min_amount + min_precision) * price + diff = next_value - min_value + + if diff > self._max_value: + self.log_once(f"Removed {pair} from whitelist, " + f"because min value change of {diff} > {self._max_value}) ", + logger.info) + return False + # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: @@ -89,7 +121,7 @@ class PriceFilter(IPairList): # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because last price > {self._max_price:.8f}", logger.info) return False diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 8347687da..552f47679 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -800,6 +800,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", None ), + ({"method": "PriceFilter", "max_value": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]", + None + ), ({"method": "PriceFilter"}, "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", None @@ -816,6 +820,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected + ({"method": "PriceFilter", "max_value": -1.00010000}, + None, + "PriceFilter requires max_value to be >= 0" + ), # OperationalException expected ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " "0.01 over the last days.'}]", From 6659a070796d3619e80951269ee3c99e5ebb39a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 06:47:58 +0200 Subject: [PATCH 411/460] Add tests for max-value filter --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- tests/plugins/test_pairlist.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index f25ab8fd0..ece9f54a8 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -33,7 +33,7 @@ class PriceFilter(IPairList): self._enabled = ((self._low_price_ratio > 0) or (self._min_price > 0) or (self._max_price > 0) or - (self._max_value)) + (self._max_value > 0)) @property def needstickers(self) -> bool: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 552f47679..5e2274ce3 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -407,6 +407,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), + ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "max_value": 0.000001}], + "USDT", ['NANO/USDT']), ([{"method": "StaticPairList"}, {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], @@ -489,6 +492,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t r'because last price < .*%$', caplog) or log_has_re(r'^Removed .* from whitelist, ' r'because last price > .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because min value change of .*', caplog) or log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", caplog)) if pairlist['method'] == 'VolumePairList': From 6aa574fa2b0095f64c946955397b4d7c152fabc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 20:58:50 +0200 Subject: [PATCH 412/460] Convert ROI result to proper json object closes #4952 --- freqtrade/optimize/hyperopt.py | 3 ++- tests/optimize/test_hyperopt.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 45f6bfb88..d28373c2f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -185,7 +185,8 @@ class Hyperopt: if HyperoptTools.has_space(self.config, 'sell'): result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} if HyperoptTools.has_space(self.config, 'roi'): - result['roi'] = self.custom_hyperopt.generate_roi_table(params) + result['roi'] = {str(k): v for k, v in + self.custom_hyperopt.generate_roi_table(params).items()} if HyperoptTools.has_space(self.config, 'stoploss'): result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} if HyperoptTools.has_space(self.config, 'trailing'): diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a0c475efb..f23d5bc9e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -649,10 +649,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'rsi-enabled': False, 'rsi-value': 0, 'trigger': 'macd_cross_signal'}, - 'roi': {0: 0.12000000000000001, - 20.0: 0.02, - 50.0: 0.01, - 110.0: 0}, + 'roi': {"0": 0.12000000000000001, + "20.0": 0.02, + "50.0": 0.01, + "110.0": 0}, 'sell': {'sell-adx-enabled': False, 'sell-adx-value': 0, 'sell-fastd-enabled': True, From 36eba0f110a2551bfdfb2c96045e93a770fcdeaa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 May 2021 21:05:48 +0200 Subject: [PATCH 413/460] Don't use "r+" memmap, but "r2 --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d28373c2f..11a1a36f2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -271,7 +271,7 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] - processed = load(self.data_pickle_file, mmap_mode='r+') + processed = load(self.data_pickle_file, mmap_mode='r') bt_results = self.backtesting.backtest( processed=processed, From 16c22c7b68527c6376ef7e03b5fad0a6efedc34c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 19:16:25 +0200 Subject: [PATCH 414/460] Add pair name to stoploss helps debugging #4972 --- freqtrade/edge/edge_positioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 0449d6ebe..4bc0d660b 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -209,7 +209,7 @@ class Edge: if pair in self._cached_pairs: return self._cached_pairs[pair].stoploss else: - logger.warning('tried to access stoploss of a non-existing pair, ' + logger.warning(f'Tried to access stoploss of non-existing pair {pair}, ' 'strategy stoploss is returned instead.') return self.strategy.stoploss From 2565f91bc2956070c1e45ef0b18f022adbd86041 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 19:33:17 +0200 Subject: [PATCH 415/460] Adjust tests to reflect new stoploss behaviour --- tests/optimize/test_backtest_detail.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 7da7709c6..9d88d166a 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -185,7 +185,7 @@ tc11 = BTContainer(data=[ [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], - [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [3, 5000, 5150, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, @@ -440,6 +440,23 @@ tc27 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] ) +# Test 28: trailing_stop should raise so candle 3 causes a stoploss +# Same case than tc11 - but candle 3 "gaps down" - the stoploss will be above the candle, +# therefore "open" will be used +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 +tc28 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, tc1, @@ -469,6 +486,7 @@ TESTS = [ tc25, tc26, tc27, + tc28, ] From 7a9853bfe1a094b655bc2923e230d92b81d51efa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 20:39:55 +0200 Subject: [PATCH 416/460] Fix "Too many open Files" exception --- freqtrade/optimize/hyperopt.py | 4 ++-- tests/optimize/test_hyperopt.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 11a1a36f2..326ae144c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -271,8 +271,8 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] - processed = load(self.data_pickle_file, mmap_mode='r') - + with self.data_pickle_file.open('rb') as f: + processed = load(f, mmap_mode='r') bt_results = self.backtesting.backtest( processed=processed, start_date=self.min_date, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f23d5bc9e..b80ede734 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -600,6 +600,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: mocker.patch('freqtrade.optimize.hyperopt.get_timerange', return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) + mocker.patch.object(Path, 'open') mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { From 75f88b466a6c29b64bb270adecc7cdd7dc039b81 Mon Sep 17 00:00:00 2001 From: axel Date: Tue, 18 May 2021 19:30:36 -0400 Subject: [PATCH 417/460] add ability to choose unit in unfilled timeout --- docs/configuration.md | 5 +++-- freqtrade/constants.py | 4 +++- freqtrade/freqtradebot.py | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 37395c5ee..eff8fe322 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,8 +68,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bfd1e72f1..5ec60eb59 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +TIMEOUT_UNITS = ['minutes', 'seconds'] DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -137,7 +138,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'buy': {'type': 'number', 'minimum': 1}, - 'sell': {'type': 'number', 'minimum': 1} + 'sell': {'type': 'number', 'minimum': 1}, + 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } }, 'bid_strategy': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0bef29dd4..fa372e6e8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -974,8 +974,9 @@ class FreqtradeBot(LoggingMixin): timeout = self.config.get('unfilledtimeout', {}).get(side) ordertime = arrow.get(order['datetime']).datetime if timeout is not None: - timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime - + timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') + timeout_kwargs = {timeout_unit: -timeout} + timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime return (order['status'] == 'open' and order['side'] == side and ordertime < timeout_threshold) return False From 082fb11bbedde8a9521f72c1018a1f2ba73357fc Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 01:54:48 +0700 Subject: [PATCH 418/460] Avoid having error `cannot set a frame with no defined index and a scalar` --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c6b05c8c6..e6172282b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -191,8 +191,8 @@ class Backtesting: data: Dict = {} # Create dict with data for pair, pair_data in processed.items(): - pair_data.loc[:, 'buy'] = 0 # cleanup from previous run - pair_data.loc[:, 'sell'] = 0 # cleanup from previous run + pair_data['buy'] = 0 # cleanup from previous run + pair_data['sell'] = 0 # cleanup from previous run df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() From 0358b5365fa2302520a4941122f0b40fecfe757d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 May 2021 06:25:40 +0200 Subject: [PATCH 419/460] Add "unfilledtimeout-unit" to full config sample --- config_full.json.example | 3 ++- freqtrade/templates/base_config.json.j2 | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 973afe2c8..24d364fdf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -23,7 +23,8 @@ "stoploss": -0.10, "unfilledtimeout": { "buy": 10, - "sell": 30 + "sell": 30, + "unit": "minutes" }, "bid_strategy": { "price_side": "bid", diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 42f088f9f..8933ebc6a 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -9,7 +9,8 @@ "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, - "sell": 30 + "sell": 30, + "unit": "minutes" }, "bid_strategy": { "price_side": "bid", From 48210170e7a562205f459dd3568f810417c021fd Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:49:25 +0700 Subject: [PATCH 420/460] wrap with is not empty --- 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 e6172282b..955b9e614 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -191,8 +191,9 @@ class Backtesting: data: Dict = {} # Create dict with data for pair, pair_data in processed.items(): - pair_data['buy'] = 0 # cleanup from previous run - pair_data['sell'] = 0 # cleanup from previous run + if not pair_data.empty: + pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist + pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() From 1b3bfb2e7fcb21914857cc103d4873377e3dd7d2 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:50:15 +0700 Subject: [PATCH 421/460] found root cause. --- freqtrade/optimize/hyperopt.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 326ae144c..0250935b9 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -349,19 +349,25 @@ class Hyperopt: def prepare_hyperopt_data(self) -> None: data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") - preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + processed: Dict[str, DataFrame] = {} + preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange, - startup_candles=self.backtesting.required_startup) - self.min_date, self.max_date = get_timerange(preprocessed) + trimed_df = trim_dataframe(df, timerange, + startup_candles=self.backtesting.required_startup) + if not trimed_df.empty: + processed[pair] = trimed_df + else: + logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') + + self.min_date, self.max_date = get_timerange(processed) logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(self.max_date - self.min_date).days} days)..') - dump(preprocessed, self.data_pickle_file) + dump(processed, self.data_pickle_file) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) From c2b9da68e14d96c1b012170a7de69be83e28352f Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:56:11 +0700 Subject: [PATCH 422/460] fix indent --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 955b9e614..0144d934b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -192,8 +192,8 @@ class Backtesting: # Create dict with data for pair, pair_data in processed.items(): if not pair_data.empty: - pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist - pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist + pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist + pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() From 6172e67fcd0693f292bf032acbb2383ce7b65c72 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Thu, 20 May 2021 11:56:31 +0700 Subject: [PATCH 423/460] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 0250935b9..3d4588145 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -357,9 +357,9 @@ class Hyperopt: trimed_df = trim_dataframe(df, timerange, startup_candles=self.backtesting.required_startup) if not trimed_df.empty: - processed[pair] = trimed_df + processed[pair] = trimed_df else: - logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') + logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') self.min_date, self.max_date = get_timerange(processed) From 0045d3a726f266f720f6229c1ae5d1c073ab99b2 Mon Sep 17 00:00:00 2001 From: Kamontat Chantrachirathumrong Date: Fri, 21 May 2021 11:18:16 +0700 Subject: [PATCH 424/460] fix wrong json key --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 900b784f2..ece0a253e 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -396,7 +396,7 @@ def main(args): sys.exit() config = load_config(args['config']) - url = config.get('api_server', {}).get('server_url', '127.0.0.1') + url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1') port = config.get('api_server', {}).get('listen_port', '8080') username = config.get('api_server', {}).get('username') password = config.get('api_server', {}).get('password') From 1a30e39222ae73ca32247ceccea1e881a437d6ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 08:06:27 +0200 Subject: [PATCH 425/460] Move squeeze into if block --- docs/strategy-advanced.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f5f706cd6..52879c7b4 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -40,7 +40,6 @@ class AwesomeStrategy(IStrategy): !!! Note If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - ## Dataframe access You may access dataframe in various strategy functions by querying it from dataprovider. @@ -67,8 +66,8 @@ class AwesomeStrategy(IStrategy): trade_candle = dataframe.loc[dataframe['date'] == trade_date] # trade_candle may be empty for trades that just opened as it is still incomplete. if trade_candle.empty: + trade_candle = trade_candle.squeeze() # <...> - trade_candle = trade_candle.squeeze() ``` !!! Warning "Using .iloc[-1]" From f398888865c732df55b0ecee3c775aba4d4a9ff4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 08:26:19 +0200 Subject: [PATCH 426/460] Refactor preprocessed trimming to seperate method --- freqtrade/data/converter.py | 22 ++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 12 ++---------- freqtrade/optimize/hyperopt.py | 12 +++--------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index af6c6a2ef..7098834ec 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime +from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList @@ -145,6 +146,27 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', return df +def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange: TimeRange, + startup_candles: int) -> Dict[str, DataFrame]: + """ + Trim startup period from analyzed dataframes + :param preprocessed: Dict of pair: dataframe + :param timerange: timerange (use start and end date if available) + :param startup_candles: Startup-candles that should be removed + :return: Dict of trimmed dataframes + """ + processed: Dict[str, DataFrame] = {} + + for pair, df in preprocessed.items(): + trimed_df = trim_dataframe(df, timerange, startup_candles=startup_candles) + if not trimed_df.empty: + processed[pair] = trimed_df + else: + logger.warning(f'{pair} has no data left after adjusting for startup candles, ' + f'skipping.') + return processed + + def order_book_to_dataframe(bids: list, asks: list) -> DataFrame: """ TODO: This should get a dedicated test diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0144d934b..aff09921c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -15,7 +15,7 @@ from freqtrade.configuration import TimeRange, remove_credentials, validate_conf from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe -from freqtrade.data.converter import trim_dataframe +from freqtrade.data.converter import trim_dataframes from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -462,15 +462,7 @@ class Backtesting: preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe - for pair in list(preprocessed): - df = preprocessed[pair] - df = trim_dataframe(df, timerange, startup_candles=self.required_startup) - if len(df) > 0: - preprocessed[pair] = df - else: - logger.warning(f'{pair} has no data left after adjusting for startup candles, ' - f'skipping.') - del preprocessed[pair] + preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup) if not preprocessed: raise OperationalException( diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3d4588145..cf5559a24 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -20,7 +20,7 @@ from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN -from freqtrade.data.converter import trim_dataframe +from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting @@ -350,16 +350,10 @@ class Hyperopt: data, timerange = self.backtesting.load_bt_data() logger.info("Dataload complete. Calculating indicators") - processed: Dict[str, DataFrame] = {} preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) + # Trim startup period from analyzed dataframe - for pair, df in preprocessed.items(): - trimed_df = trim_dataframe(df, timerange, - startup_candles=self.backtesting.required_startup) - if not trimed_df.empty: - processed[pair] = trimed_df - else: - logger.warn(f'Pair {pair} got removed because triming dataframe left nothing') + processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) self.min_date, self.max_date = get_timerange(processed) From 96ea10e56213000a2534e7433515b3ef9045cef6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 08:52:56 +0200 Subject: [PATCH 427/460] Fix circular import in hyperopt --- freqtrade/data/converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 7098834ec..ffee0c52c 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,7 +10,6 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList @@ -146,7 +145,7 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', return df -def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange: TimeRange, +def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange, startup_candles: int) -> Dict[str, DataFrame]: """ Trim startup period from analyzed dataframes From 0e6c1d28f466893d70008275de92198b2f17a446 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 09:32:18 +0200 Subject: [PATCH 428/460] Fix cleanup CI by updating action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4169661c6..5a0837eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.3.2 + uses: rokroskar/workflow-run-cleanup-action@v0.3.3 if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From edcfa940937d0141f74b1bdd4f8161f76746fb83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 15 May 2021 11:26:22 +0300 Subject: [PATCH 429/460] Include zero duration trades in backtesting report. --- docs/backtesting.md | 1 + freqtrade/optimize/optimize_reports.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ee9926f32..8b3aa8cc1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -413,6 +413,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. +- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2be3c3e62..b509f216f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -208,6 +208,8 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: winning_trades = results.loc[results['profit_ratio'] > 0] draw_trades = results.loc[results['profit_ratio'] == 0] losing_trades = results.loc[results['profit_ratio'] < 0] + zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) & + (results['sell_reason'] == 'trailing_stop_loss')]) return { 'wins': len(winning_trades), @@ -219,6 +221,7 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: if not winning_trades.empty else timedelta()), 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) if not losing_trades.empty else timedelta()), + 'zero_duration_trades': zero_duration_trades, } @@ -496,6 +499,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio']) worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio']) + zero_duration_trades_percent =\ + 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] metrics = [ ('Backtesting from', strat_results['backtest_start']), ('Backtesting to', strat_results['backtest_end']), @@ -508,7 +513,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), + ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"), ('Trades per day', strat_results['trades_per_day']), ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], strat_results['stake_currency'])), @@ -532,6 +537,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), + ('Zero Duration Trades', f"{zero_duration_trades_percent:.1f}% " + f"({strat_results['zero_duration_trades']})"), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], From e1dc1357ce37243f4f8ff214480d339b7249154c Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 15 May 2021 12:00:01 +0300 Subject: [PATCH 430/460] Add drawdown column to strategy summary table. --- docs/backtesting.md | 10 +++++----- freqtrade/optimize/optimize_reports.py | 18 ++++++++++++++++- tests/optimize/test_optimize_reports.py | 26 ++++++++++++++++--------- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 8b3aa8cc1..5ee0b8a49 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -473,11 +473,11 @@ There will be an additional table comparing win/losses of the different strategi Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. ``` -=========================================================== STRATEGY SUMMARY =========================================================== -| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:| -| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | -| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | +=========================================================== STRATEGY SUMMARY ========================================================================= +| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | +|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| +| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | +| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | ``` ## Next step diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b509f216f..d8ef92e7c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -164,6 +164,17 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]: tabular_data.append(_generate_result_line( results['results'], results['config']['dry_run_wallet'], strategy) ) + try: + max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'], + value_col='profit_ratio') + max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(results['results'], + value_col='profit_abs') + except ValueError: + max_drawdown_per = 0 + max_drawdown_abs = 0 + tabular_data[-1]['max_drawdown_per'] = round(max_drawdown_per * 100, 2) + tabular_data[-1]['max_drawdown_abs'] = \ + round_coin_value(max_drawdown_abs, results['config']['stake_currency'], False) return tabular_data @@ -485,10 +496,15 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: """ floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header('Strategy', stake_currency) + # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless + # therefore we slip this column in only for strategy summary here. + headers.append(f'Drawdown {stake_currency}') + headers.append('Drawdown %') output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'], + t['max_drawdown_abs'], t['max_drawdown_per'] ] for t in strategy_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 1afa5c59c..e87cec6fa 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,3 +1,4 @@ +import datetime import re from datetime import timedelta from pathlib import Path @@ -325,9 +326,12 @@ def test_text_table_strategy(default_conf): default_conf['max_open_trades'] = 2 default_conf['dry_run_wallet'] = 3 results = {} + date = datetime.datetime(year=2020, month=1, day=1, hour=12, minute=30) + delta = datetime.timedelta(days=1) results['TestStrategy1'] = {'results': pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'close_date': [date, date + delta, date + delta * 2], 'profit_ratio': [0.1, 0.2, 0.3], 'profit_abs': [0.2, 0.4, 0.5], 'trade_duration': [10, 30, 10], @@ -340,6 +344,7 @@ def test_text_table_strategy(default_conf): results['TestStrategy2'] = {'results': pd.DataFrame( { 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], + 'close_date': [date, date + delta, date + delta * 2], 'profit_ratio': [0.4, 0.2, 0.3], 'profit_abs': [0.4, 0.4, 0.5], 'trade_duration': [15, 30, 15], @@ -351,18 +356,21 @@ def test_text_table_strategy(default_conf): ), 'config': default_conf} result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot' - ' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' - '|---------------+--------+----------------+----------------+------------------+' - '----------------+----------------+--------+---------+----------|\n' - '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' - ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n' - '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' - ' 43.33 | 0:20:00 | 3 | 0 | 0 |' + '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC ' + '| Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown BTC ' + '| Drawdown % |\n' + '|---------------+--------+----------------+----------------+------------------' + '+----------------+----------------+--------+---------+----------+----------------' + '+--------------|\n' + '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 ' + '| 36.67 | 0:17:00 | 3 | 0 | 0 | 0 ' + '| 0 |\n' + '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 ' + '| 43.33 | 0:20:00 | 3 | 0 | 0 | 0 ' + '| 0 |' ) strategy_results = generate_strategy_comparison(all_results=results) - assert text_table_strategy(strategy_results, 'BTC') == result_str From debd98ad9a6278ca026bdb7fb32f287687f994ee Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 21 May 2021 11:29:22 +0300 Subject: [PATCH 431/460] Make results table more compact by merging win/draw/loss columns and drawdown abs/% into single columns. --- freqtrade/optimize/optimize_reports.py | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d8ef92e7c..68f315926 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -43,7 +43,7 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: Generate floatformat (goes in line with _generate_result_line()) """ return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', - '.2f', 'd', 'd', 'd', 'd'] + '.2f', 'd', 's', 's'] def _get_line_header(first_column: str, stake_currency: str) -> List[str]: @@ -52,7 +52,11 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: """ return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Wins', 'Draws', 'Losses'] + 'Win Draw Loss'] + + +def _generate_wins_draws_losses(wins, draws, losses): + return f'{wins:>4} {draws:>4} {losses:>4}' def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -451,7 +455,8 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st floatfmt = _get_line_floatfmt(stake_currency) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) ] for t in pair_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, @@ -468,9 +473,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren headers = [ 'Sell Reason', 'Sells', - 'Wins', - 'Draws', - 'Losses', + 'Win Draws Loss', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', @@ -478,7 +481,8 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] output = [[ - t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], + t['sell_reason'], t['trades'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), t['profit_mean_pct'], t['profit_sum_pct'], round_coin_value(t['profit_total_abs'], stake_currency, False), t['profit_total_pct'], @@ -498,14 +502,20 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: headers = _get_line_header('Strategy', stake_currency) # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless # therefore we slip this column in only for strategy summary here. - headers.append(f'Drawdown {stake_currency}') - headers.append('Drawdown %') + headers.append(f'Drawdown') + + # Align drawdown string on the center two space separator. + drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results] + dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results]) + dd_pad_per = max([len(dd) for dd in drawdown]) + drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%' + for t, dd in zip(strategy_results, drawdown)] output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'], - t['max_drawdown_abs'], t['max_drawdown_per'] - ] for t in strategy_results] + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown] + for t, drawdown in zip(strategy_results, drawdown)] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") From 981b2df7caec96565ba66d2abb7201326fcfaa96 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 21 May 2021 12:00:24 +0300 Subject: [PATCH 432/460] Include win:loss ratio in results tables. --- freqtrade/optimize/optimize_reports.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 68f315926..0474ec1bf 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -52,11 +52,17 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: """ return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Win Draw Loss'] + 'Win Draw Loss Win%'] def _generate_wins_draws_losses(wins, draws, losses): - return f'{wins:>4} {draws:>4} {losses:>4}' + if wins > 0 and losses == 0: + wl_ratio = '100' + elif wins == 0: + wl_ratio = '0' + else: + wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' + return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -473,7 +479,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren headers = [ 'Sell Reason', 'Sells', - 'Win Draws Loss', + 'Win Draws Loss Win%', 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', From dfa412f0de369ea8302a7c59b7fae731e08a0e2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 13:24:13 +0200 Subject: [PATCH 433/460] Fix typo in filter --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index ece9f54a8..5b5afb557 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -107,7 +107,7 @@ class PriceFilter(IPairList): if diff > self._max_value: self.log_once(f"Removed {pair} from whitelist, " - f"because min value change of {diff} > {self._max_value}) ", + f"because min value change of {diff} > {self._max_value}.", logger.info) return False From 9537d9f4e2b9c3302866a2652a516a41b5ff7335 Mon Sep 17 00:00:00 2001 From: Nicolas Menescardi Date: Fri, 21 May 2021 11:27:22 -0300 Subject: [PATCH 434/460] Update strategy-customization.md Fix typo: 'This will method will...' -> 'This method will...' --- docs/strategy-customization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 49456c047..cfea60d22 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -159,7 +159,7 @@ Edit the method `populate_buy_trend()` in your strategy file to update your buy It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. -This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". +This method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". Sample from `user_data/strategies/sample_strategy.py`: @@ -193,7 +193,7 @@ Please note that the sell-signal is only used if `use_sell_signal` is set to tru It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. -This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". +This method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". Sample from `user_data/strategies/sample_strategy.py`: From 45e2621505fbfe748b5507dccdb697bebd5fbe34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 19:19:38 +0200 Subject: [PATCH 435/460] Add minimum-filled protection for buy cancels --- freqtrade/freqtradebot.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fa372e6e8..1c3a759f4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1045,6 +1045,16 @@ class FreqtradeBot(LoggingMixin): # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('cancelled', 'canceled', 'closed'): + filled_val = order.get('filled', 0.0) or 0.0 + filled_stake = filled_val * trade.open_rate + minstake = self.exchange.get_min_pair_stake_amount( + trade.pair, trade.open_rate, self.strategy.stoploss) + + if filled_val > 0 and filled_stake < minstake: + logger.warning( + f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"as the filled amount of {filled_val} would result in an unsellable trade.") + return False corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. From 4e94d3d3e5bb84425a875d298dc66dbba81ed9b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 19:32:26 +0200 Subject: [PATCH 436/460] Add test for too small buy check --- tests/test_freqtradebot.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index df0715111..4d9284a2f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2431,13 +2431,22 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non freqtrade._notify_buy_cancel = MagicMock() trade = MagicMock() - trade.pair = 'LTC/ETH' + trade.pair = 'LTC/USDT' + trade.open_rate = 200 limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 + cancel_order_mock.reset_mock() + caplog.clear() + limit_buy_order['filled'] = 0.01 + assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert cancel_order_mock.call_count == 0 + assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) + + caplog.clear() cancel_order_mock.reset_mock() limit_buy_order['filled'] = 2 assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) @@ -2492,7 +2501,8 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, freqtrade._notify_buy_cancel = MagicMock() trade = MagicMock() - trade.pair = 'LTC/ETH' + trade.pair = 'LTC/USDT' + trade.open_rate = 200 limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] From 6acb2eb2b60c42a18a02b886f75c0d75da17489a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:35:39 +0200 Subject: [PATCH 437/460] Add average column to orders table --- freqtrade/persistence/migrations.py | 32 ++++++++++++++++++++++++++--- freqtrade/persistence/models.py | 2 ++ tests/test_persistence.py | 16 +++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 961363b0e..d89256baf 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -123,6 +123,27 @@ def migrate_open_orders_to_trades(engine): """) +def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): + # Schema migration necessary + engine.execute(f"alter table orders rename to {table_back_name}") + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + engine.execute(f"drop index {index['name']}") + + # let SQLAlchemy create the schema as required + decl_base.metadata.create_all(engine) + + engine.execute(f""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, average, remaining, cost, order_date, + order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, null average, remaining, cost, order_date, + order_filled_date, order_update_date + from {table_back_name} + """) + + def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary @@ -145,6 +166,11 @@ def check_migrate(engine, decl_base, previous_tables) -> None: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: - pass - # Empty for now - as there is only one iteration of the orders table so far. - # table_back_name = get_backup_name(tabs, 'orders_bak') + cols_order = inspector.get_columns('orders') + + if not has_column(cols_order, 'average'): + tabs = get_table_names_for_table(inspector, 'orders') + # Empty for now - as there is only one iteration of the orders table so far. + table_back_name = get_backup_name(tabs, 'orders_bak') + + migrate_orders_table(decl_base, inspector, engine, table_back_name, cols) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8d2c9d1d3..428455ff8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -122,6 +122,7 @@ class Order(_DECL_BASE): order_type = Column(String, nullable=True) side = Column(String, nullable=True) price = Column(Float, nullable=True) + average = Column(Float, nullable=True) amount = Column(Float, nullable=True) filled = Column(Float, nullable=True) remaining = Column(Float, nullable=True) @@ -150,6 +151,7 @@ class Order(_DECL_BASE): self.price = order.get('price', self.price) self.amount = order.get('amount', self.amount) self.filled = order.get('filled', self.filled) + self.average = order.get('average', self.average) self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) if 'timestamp' in order and order['timestamp'] is not None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8470e12c2..a38c12470 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -627,6 +627,22 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'stop_order_id222' assert orders[1].ft_order_side == 'stoploss' + caplog.clear() + # Drop latest column + engine.execute("alter table orders drop average") + # Run init to test migration + init_db(default_conf['db_url'], default_conf['dry_run']) + + assert log_has("trying orders_bak0", caplog) + + orders = Order.query.all() + assert len(orders) == 2 + assert orders[0].order_id == 'buy_order' + assert orders[0].ft_order_side == 'buy' + + assert orders[1].order_id == 'stop_order_id222' + assert orders[1].ft_order_side == 'stoploss' + def test_migrate_mid_state(mocker, default_conf, fee, caplog): """ From 41e3233bab3bc4a79b4dc595aa80d88d1c4bf8b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:44:11 +0200 Subject: [PATCH 438/460] Fix failing test --- tests/test_persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a38c12470..38fe3ff94 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -629,7 +629,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): caplog.clear() # Drop latest column - engine.execute("alter table orders drop average") + engine.execute("alter table orders drop column average") # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) From a7216e627988e62c7883eef4282785c30b3b5048 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:53:38 +0200 Subject: [PATCH 439/460] SQLite does not know drop column --- tests/test_persistence.py | 48 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 38fe3ff94..2949fdfe9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock import arrow import pytest -from sqlalchemy import create_engine +from sqlalchemy import create_engine, inspect from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException @@ -629,11 +629,53 @@ def test_migrate_new(mocker, default_conf, fee, caplog): caplog.clear() # Drop latest column - engine.execute("alter table orders drop column average") + engine.execute("alter table orders rename to orders_bak") + inspector = inspect(engine) + + for index in inspector.get_indexes('orders_bak'): + engine.execute(f"drop index {index['name']}") + # Recreate table + engine.execute(""" + CREATE TABLE orders ( + id INTEGER NOT NULL, + ft_trade_id INTEGER, + ft_order_side VARCHAR NOT NULL, + ft_pair VARCHAR NOT NULL, + ft_is_open BOOLEAN NOT NULL, + order_id VARCHAR NOT NULL, + status VARCHAR, + symbol VARCHAR, + order_type VARCHAR, + side VARCHAR, + price FLOAT, + amount FLOAT, + filled FLOAT, + remaining FLOAT, + cost FLOAT, + order_date DATETIME, + order_filled_date DATETIME, + order_update_date DATETIME, + PRIMARY KEY (id), + CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), + FOREIGN KEY(ft_trade_id) REFERENCES trades (id) + ) + """ + ) + + engine.execute(""" + insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, remaining, cost, order_date, + order_filled_date, order_update_date) + select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, + symbol, order_type, side, price, amount, filled, remaining, cost, order_date, + order_filled_date, order_update_date + from orders_bak + """) + # Run init to test migration init_db(default_conf['db_url'], default_conf['dry_run']) - assert log_has("trying orders_bak0", caplog) + assert log_has("trying orders_bak1", caplog) orders = Order.query.all() assert len(orders) == 2 From 44bbc0718e2d568c35e35344a5945a5f8971c796 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 May 2021 20:54:18 +0200 Subject: [PATCH 440/460] CLosing bracket --- tests/test_persistence.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2949fdfe9..669f220bb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -659,8 +659,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) - """ - ) + """) engine.execute(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, From 5285cd69b4d19bdaaeb267e0f24065a078b05787 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 10:12:03 +0200 Subject: [PATCH 441/460] Add documentation for Postgres and Mysql --- docs/sql_cheatsheet.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 569af33ff..477396931 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -19,7 +19,7 @@ The freqtrade docker image does contain sqlite3, so you can edit the database wi ``` bash docker-compose exec freqtrade /bin/bash -sqlite3 .sqlite +sqlite3 .sqlite ``` ## Open the DB @@ -99,3 +99,32 @@ DELETE FROM trades WHERE id = 31; !!! Warning This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause. + +## Use a different database system + +!!! Warning + By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems. + +### PostgreSQL + +Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems. + +Installation: +`pip install psycopg2` + +Usage: +`... --db-url postgresql+psycopg2://:@localhost:5432/` + +Freqtrade will automatically create the tables necessary upon startup. + +If you're running different instances of Freqtrade, you must either setup one database per Instance or use different users / schemas for your connections. + +### MariaDB / MySQL + +Freqtrade supports MariaDB by using SQLAlchemy, which supports multiple different database systems. + +Installation: +`pip install pymysql` + +Usage: +`... --db-url mysql+pymysql://:@localhost:3306/` From cc064f157422fe7b35b3208b25e66c34cbd10b3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 10:12:23 +0200 Subject: [PATCH 442/460] String columns should have a max-length defined otherwise MySql will not work. --- freqtrade/persistence/models.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 428455ff8..f2e7a10c4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -112,15 +112,15 @@ class Order(_DECL_BASE): trade = relationship("Trade", back_populates="orders") - ft_order_side = Column(String, nullable=False) - ft_pair = Column(String, nullable=False) + ft_order_side = Column(String(25), nullable=False) + ft_pair = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) - order_id = Column(String, nullable=False, index=True) - status = Column(String, nullable=True) - symbol = Column(String, nullable=True) - order_type = Column(String, nullable=True) - side = Column(String, nullable=True) + order_id = Column(String(255), nullable=False, index=True) + status = Column(String(255), nullable=True) + symbol = Column(String(25), nullable=True) + order_type = Column(String(50), nullable=True) + side = Column(String(25), nullable=True) price = Column(Float, nullable=True) average = Column(Float, nullable=True) amount = Column(Float, nullable=True) @@ -658,15 +658,15 @@ class Trade(_DECL_BASE, LocalTrade): orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") - exchange = Column(String, nullable=False) - pair = Column(String, nullable=False, index=True) + exchange = Column(String(25), nullable=False) + pair = Column(String(25), nullable=False, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True) fee_open = Column(Float, nullable=False, default=0.0) fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String, nullable=True) + fee_open_currency = Column(String(25), nullable=True) fee_close = Column(Float, nullable=False, default=0.0) fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String, nullable=True) + fee_close_currency = Column(String(25), nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) # open_trade_value - calculated via _calc_open_trade_value @@ -680,7 +680,7 @@ class Trade(_DECL_BASE, LocalTrade): amount_requested = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) - open_order_id = Column(String) + open_order_id = Column(String(255)) # absolute value of the stop loss stop_loss = Column(Float, nullable=True, default=0.0) # percentage value of the stop loss @@ -690,16 +690,16 @@ class Trade(_DECL_BASE, LocalTrade): # percentage value of the initial stop loss initial_stop_loss_pct = Column(Float, nullable=True) # stoploss order id which is on exchange - stoploss_order_id = Column(String, nullable=True, index=True) + stoploss_order_id = Column(String(255), nullable=True, index=True) # last update time of the stoploss order on exchange stoploss_last_update = Column(DateTime, nullable=True) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String, nullable=True) - sell_order_status = Column(String, nullable=True) - strategy = Column(String, nullable=True) + sell_reason = Column(String(100), nullable=True) + sell_order_status = Column(String(100), nullable=True) + strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): @@ -856,8 +856,8 @@ class PairLock(_DECL_BASE): id = Column(Integer, primary_key=True) - pair = Column(String, nullable=False, index=True) - reason = Column(String, nullable=True) + pair = Column(String(25), nullable=False, index=True) + reason = Column(String(255), nullable=True) # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) From a0921ec75342f036b2f30c5a22478181f53820a0 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Mon, 17 May 2021 11:31:19 +0200 Subject: [PATCH 443/460] Add backoff timer for coingecko API Set a future timestamp when we should retry getting coingecko data. This fixes conversion from stake to fiat when running multiple bots as we don't simply accept the 429 error from Coingecko but handle it. --- freqtrade/rpc/fiat_convert.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 380070deb..908f7adf0 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -4,10 +4,12 @@ e.g BTC to USD """ import logging -from typing import Dict +import datetime +from typing import Dict from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI +from requests.exceptions import RequestException from freqtrade.constants import SUPPORTED_FIAT @@ -25,6 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} + _backoff = int def __new__(cls): """ @@ -47,8 +50,13 @@ class CryptoToFiatConverter: def _load_cryptomap(self) -> None: try: coinlistings = self._coingekko.get_coins_list() - # Create mapping table from synbol to coingekko_id + # Create mapping table from symbol to coingekko_id self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} + except RequestException as request_exception: + if "429" in str(request_exception): + logger.warning("Too many requests for Coingecko API, backing off and trying again later.") + # Set backoff timestamp to 60 seconds in the future + self._backoff = datetime.datetime.now().timestamp() + 60 except (Exception) as exception: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") @@ -127,6 +135,9 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 + if self._cryptomap == {} and self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + if crypto_symbol not in self._cryptomap: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) From 8842e0d161fec054bfec2f2ec4d3e2455dd3a9ef Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Mon, 17 May 2021 12:05:25 +0200 Subject: [PATCH 444/460] Fix flake8 error in fiat_convert --- freqtrade/rpc/fiat_convert.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 908f7adf0..2088b0258 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -54,7 +54,8 @@ class CryptoToFiatConverter: self._cryptomap = {x['symbol']: x['id'] for x in coinlistings} except RequestException as request_exception: if "429" in str(request_exception): - logger.warning("Too many requests for Coingecko API, backing off and trying again later.") + logger.warning( + "Too many requests for Coingecko API, backing off and trying again later.") # Set backoff timestamp to 60 seconds in the future self._backoff = datetime.datetime.now().timestamp() + 60 except (Exception) as exception: From ab6bfbad12791f65f784c9cb2ba39998269d7cfb Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 11:51:43 +0200 Subject: [PATCH 445/460] Handle RequestExceptions that are not 429s in _load_cryptomap --- freqtrade/rpc/fiat_convert.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 2088b0258..5b73663c8 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -58,6 +58,13 @@ class CryptoToFiatConverter: "Too many requests for Coingecko API, backing off and trying again later.") # Set backoff timestamp to 60 seconds in the future self._backoff = datetime.datetime.now().timestamp() + 60 + return + # If the request is not a 429 error we want to raise the normal error + logger.error( + "Could not load FIAT Cryptocurrency map for the following problem: {}".format( + request_exception + ) + ) except (Exception) as exception: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") From 6e05f856b470c26e5682bf8495e34bac0e78b042 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 11:55:03 +0200 Subject: [PATCH 446/460] Abort _find_price when cryptomap is empty after retry --- freqtrade/rpc/fiat_convert.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 5b73663c8..4987f8ce1 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -145,6 +145,9 @@ class CryptoToFiatConverter: if self._cryptomap == {} and self._backoff <= datetime.datetime.now().timestamp(): self._load_cryptomap() + # return 0.0 if we still dont have data to check, no reason to proceed + if self._cryptomap == {}: + return 0.0 if crypto_symbol not in self._cryptomap: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) From e4ca944597099ca7505d22f333c7759ef2971a09 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 12:04:24 +0200 Subject: [PATCH 447/460] Add tests for coingecko backoff --- tests/rpc/test_fiat_convert.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 2d43addff..7f345a1a2 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock +import datetime import pytest from requests.exceptions import RequestException @@ -21,6 +22,12 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._backoff = 0 + mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._load_cryptomap', + return_value=None) + assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 0.0 + with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC') @@ -115,6 +122,24 @@ def test_fiat_convert_without_network(mocker): CryptoToFiatConverter._coingekko = cmc_temp +def test_fiat_too_many_requests_response(mocker, caplog): + # Because CryptoToFiatConverter is a Singleton we reset the listings + req_exception = "429 Too Many Requests" + listmock = MagicMock(return_value="{}", side_effect=RequestException(req_exception)) + mocker.patch.multiple( + 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', + get_coins_list=listmock, + ) + # with pytest.raises(RequestEsxception): + fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._load_cryptomap() + + length_cryptomap = len(fiat_convert._cryptomap) + assert length_cryptomap == 0 + assert fiat_convert._backoff > datetime.datetime.now().timestamp() + assert log_has('Too many requests for Coingecko API, backing off and trying again later.', caplog) + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") @@ -132,7 +157,6 @@ def test_fiat_invalid_response(mocker, caplog): assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', caplog) - def test_convert_amount(mocker): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) From 21d986710dfabbcd40fe8e4555731974cccc3188 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Sat, 22 May 2021 13:26:59 +0200 Subject: [PATCH 448/460] Fix missing `not` in `empty` check See discussing here: https://github.com/freqtrade/freqtrade/pull/4963#discussion_r633457596 seems that request was only partially implemented --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 52879c7b4..cb759eb2f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -65,7 +65,7 @@ class AwesomeStrategy(IStrategy): # Look up trade candle. trade_candle = dataframe.loc[dataframe['date'] == trade_date] # trade_candle may be empty for trades that just opened as it is still incomplete. - if trade_candle.empty: + if not trade_candle.empty: trade_candle = trade_candle.squeeze() # <...> ``` From f8cdd6475cc60d15791db7e0fd6de665496f2812 Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 13:43:33 +0200 Subject: [PATCH 449/460] Reduce warnings when waiting for coingecko backoff --- freqtrade/rpc/fiat_convert.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 4987f8ce1..c4c5b5e25 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -143,10 +143,13 @@ class CryptoToFiatConverter: if crypto_symbol == fiat_symbol: return 1.0 - if self._cryptomap == {} and self._backoff <= datetime.datetime.now().timestamp(): - self._load_cryptomap() - # return 0.0 if we still dont have data to check, no reason to proceed - if self._cryptomap == {}: + if self._cryptomap == {}: + if self._backoff <= datetime.datetime.now().timestamp(): + self._load_cryptomap() + # return 0.0 if we still dont have data to check, no reason to proceed + if self._cryptomap == {}: + return 0.0 + else: return 0.0 if crypto_symbol not in self._cryptomap: From be1385617152a95af6764c4100782f33c6ea07ab Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 13:43:48 +0200 Subject: [PATCH 450/460] Fix flake8 error in test_fiat_convert --- tests/rpc/test_fiat_convert.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 7f345a1a2..e8b05024f 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -138,7 +138,11 @@ def test_fiat_too_many_requests_response(mocker, caplog): length_cryptomap = len(fiat_convert._cryptomap) assert length_cryptomap == 0 assert fiat_convert._backoff > datetime.datetime.now().timestamp() - assert log_has('Too many requests for Coingecko API, backing off and trying again later.', caplog) + assert log_has( + 'Too many requests for Coingecko API, backing off and trying again later.', + caplog + ) + def test_fiat_invalid_response(mocker, caplog): # Because CryptoToFiatConverter is a Singleton we reset the listings @@ -157,6 +161,7 @@ def test_fiat_invalid_response(mocker, caplog): assert log_has_re('Could not load FIAT Cryptocurrency map for the following problem: .*', caplog) + def test_convert_amount(mocker): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) From 25cc4eae96b0a6fd18047ce98da9a8aef6073b76 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 21 May 2021 12:31:16 +0300 Subject: [PATCH 451/460] Fix tests that broke after table formatting changed. --- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_optimize_reports.py | 52 ++++++++++++------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0474ec1bf..bda531d0d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -508,7 +508,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: headers = _get_line_header('Strategy', stake_currency) # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless # therefore we slip this column in only for strategy summary here. - headers.append(f'Drawdown') + headers.append('Drawdown') # Align drawdown string on the center two space separator. drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e87cec6fa..e0ff2f219 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -39,14 +39,14 @@ def test_text_table_bt_results(): ) result_str = ( - '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' - ' Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' - '|---------+--------+----------------+----------------+------------------+' - '----------------+----------------+--------+---------+----------|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 |' - ' 15.00 | 0:20:00 | 2 | 0 | 0 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' - ' 15.00 | 0:20:00 | 2 | 0 | 0 |' + '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |' + ' Avg Duration | Win Draw Loss Win% |\n' + '|---------+--------+----------------+----------------+------------------+----------------+' + '----------------+-------------------------|\n' + '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' + ' 0:20:00 | 2 0 0 100 |\n' + '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' + ' 0:20:00 | 2 0 0 100 |' ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', @@ -271,14 +271,14 @@ def test_text_table_sell_reason(): ) result_str = ( - '| Sell Reason | Sells | Wins | Draws | Losses |' - ' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n' - '|---------------+---------+--------+---------+----------+' - '----------------+----------------+------------------+----------------|\n' - '| roi | 2 | 2 | 0 | 0 |' - ' 15 | 30 | 0.6 | 15 |\n' - '| stop_loss | 1 | 0 | 0 | 1 |' - ' -10 | -10 | -0.2 | -5 |' + '| Sell Reason | Sells | Win Draws Loss Win% | Avg Profit % | Cum Profit % |' + ' Tot Profit BTC | Tot Profit % |\n' + '|---------------+---------+--------------------------+----------------+----------------+' + '------------------+----------------|\n' + '| roi | 2 | 2 0 0 100 | 15 | 30 |' + ' 0.6 | 15 |\n' + '| stop_loss | 1 | 0 0 1 0 | -10 | -10 |' + ' -0.2 | -5 |' ) sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, @@ -356,18 +356,14 @@ def test_text_table_strategy(default_conf): ), 'config': default_conf} result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC ' - '| Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown BTC ' - '| Drawdown % |\n' - '|---------------+--------+----------------+----------------+------------------' - '+----------------+----------------+--------+---------+----------+----------------' - '+--------------|\n' - '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 ' - '| 36.67 | 0:17:00 | 3 | 0 | 0 | 0 ' - '| 0 |\n' - '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 ' - '| 43.33 | 0:20:00 | 3 | 0 | 0 | 0 ' - '| 0 |' + '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' + ' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' + '|---------------+--------+----------------+----------------+------------------+' + '----------------+----------------+-------------------------+-----------------------|\n' + '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' + ' 36.67 | 0:17:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |\n' + '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' + ' 43.33 | 0:20:00 | 3 0 0 100 | 0.00000000 BTC 0.00% |' ) strategy_results = generate_strategy_comparison(all_results=results) From 08c707e0cf601210eb206820a2c03091a1317a2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 15:38:13 +0200 Subject: [PATCH 452/460] Update docs with new format --- docs/backtesting.md | 60 +++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 5ee0b8a49..e720206bb 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -237,29 +237,29 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -========================================================= BACKTESTING REPORT ======================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:| -| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 0 | 21 | -| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 0 | 8 | -| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 0 | 14 | -| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 0 | 7 | -| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 0 | 10 | -| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 0 | 20 | -| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 0 | 15 | -| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 0 | 17 | -| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 0 | 18 | -| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 0 | 9 | -| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 0 | 21 | -| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 0 | 7 | -| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 0 | 13 | -| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 0 | 5 | -| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 0 | 9 | -| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 0 | 11 | -| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 0 | 23 | -| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 0 | 15 | -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | -========================================================= SELL REASON STATS ========================================================= +========================================================= BACKTESTING REPORT ========================================================== +| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | +|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| +| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | +| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | +| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | +| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | +| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | +| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | +| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | +| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | +| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | +| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | +| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | +| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | +| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | +| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | +| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | +| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | +| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | +| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | +========================================================= SELL REASON STATS ========================================================== | Sell Reason | Sells | Wins | Draws | Losses | |:-------------------|--------:|------:|-------:|--------:| | trailing_stop_loss | 205 | 150 | 0 | 55 | @@ -267,11 +267,11 @@ A backtesting result will look like that: | sell_signal | 56 | 36 | 0 | 20 | | force_sell | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:| -| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 | -| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 | -| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 | +| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | +|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| +| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | +| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | +| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | =============== SUMMARY METRICS =============== | Metric | Value | |-----------------------+---------------------| @@ -297,6 +297,7 @@ A backtesting result will look like that: | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Zero Duration Trades | 4.6% (20) | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -318,7 +319,7 @@ The last line will give you the overall performance of your strategy, here: ``` -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | ``` The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has @@ -384,6 +385,7 @@ It contains some useful key metrics about performance of your strategy on backte | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Zero Duration Trades | 4.6% (20) | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | From 0693458507953787bff2599d1f0117c58049cc0a Mon Sep 17 00:00:00 2001 From: "A. Schueler" Date: Sat, 22 May 2021 16:26:58 +0200 Subject: [PATCH 453/460] Update freqtrade/rpc/fiat_convert.py --- freqtrade/rpc/fiat_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index c4c5b5e25..116f9b156 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -27,7 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} - _backoff = int + _backoff: int = 0 def __new__(cls): """ From a7bd8b0aa55a824506cfa2aa5451bb7341f10752 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 17:03:16 +0200 Subject: [PATCH 454/460] Fix exception in plotting when no trades where generated as seen in #4981 --- freqtrade/data/btanalysis.py | 50 +++++++++++++++++++----------------- freqtrade/plot/plotting.py | 10 +++++--- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e07b0a40f..e7af5eab8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -156,33 +156,35 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non data = data['strategy'][strategy]['trades'] df = pd.DataFrame(data) - df['open_date'] = pd.to_datetime(df['open_date'], - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - utc=True, - infer_datetime_format=True - ) + if not df.empty: + df['open_date'] = pd.to_datetime(df['open_date'], + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + utc=True, + infer_datetime_format=True + ) else: # old format - only with lists. df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD) - - df['open_date'] = pd.to_datetime(df['open_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - # Create compatibility with new format - df['profit_abs'] = df['close_rate'] - df['open_rate'] - if 'profit_ratio' not in df.columns: - df['profit_ratio'] = df['profit_percent'] - df = df.sort_values("open_date").reset_index(drop=True) + if not df.empty: + df['open_date'] = pd.to_datetime(df['open_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + # Create compatibility with new format + df['profit_abs'] = df['close_rate'] - df['open_rate'] + if not df.empty: + if 'profit_ratio' not in df.columns: + df['profit_ratio'] = df['profit_percent'] + df = df.sort_values("open_date").reset_index(drop=True) return df diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d5a729ee1..bb4283406 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -77,7 +77,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): ) except ValueError as e: raise OperationalException(e) from e - trades = trim_dataframe(trades, timerange, 'open_date') + if not trades.empty: + trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, "trades": trades, @@ -540,8 +541,11 @@ def load_and_plot_trades(config: Dict[str, Any]): df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) df_analyzed = trim_dataframe(df_analyzed, timerange) - trades_pair = trades.loc[trades['pair'] == pair] - trades_pair = extract_trades_of_period(df_analyzed, trades_pair) + if not trades.empty: + trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = extract_trades_of_period(df_analyzed, trades_pair) + else: + trades_pair = trades fig = generate_candlestick_graph( pair=pair, From 765c824bfcd913ef072ed135ee022dba6e275575 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 17:15:35 +0200 Subject: [PATCH 455/460] isort --- freqtrade/rpc/fiat_convert.py | 6 +++--- tests/rpc/test_fiat_convert.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 116f9b156..5ae20afa1 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -3,10 +3,10 @@ Module that define classes to convert Crypto-currency to FIAT e.g BTC to USD """ -import logging import datetime - +import logging from typing import Dict + from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI from requests.exceptions import RequestException @@ -27,7 +27,7 @@ class CryptoToFiatConverter: _coingekko: CoinGeckoAPI = None _cryptomap: Dict = {} - _backoff: int = 0 + _backoff: float = 0.0 def __new__(cls): """ diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index e8b05024f..5174f9416 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,9 +1,9 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 +import datetime from unittest.mock import MagicMock -import datetime import pytest from requests.exceptions import RequestException From db985cbc2ea4bfa29b2d605ca6c2c0a0d152e579 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 23 May 2021 09:24:50 +0300 Subject: [PATCH 456/460] Fix hyperopt-show failing to display old results with missing new fields. --- freqtrade/optimize/optimize_reports.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index bda531d0d..ce6758210 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -531,8 +531,18 @@ def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio']) worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio']) - zero_duration_trades_percent =\ - 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] + + # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show + # command stores these results and newer version of freqtrade must be able to handle old + # results with missing new fields. + zero_duration_trades = '--' + + if 'zero_duration_trades' in strat_results: + zero_duration_trades_per = \ + 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] + zero_duration_trades = f'{zero_duration_trades_per}% ' \ + f'({strat_results["zero_duration_trades"]})' + metrics = [ ('Backtesting from', strat_results['backtest_start']), ('Backtesting to', strat_results['backtest_end']), @@ -569,8 +579,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), - ('Zero Duration Trades', f"{zero_duration_trades_percent:.1f}% " - f"({strat_results['zero_duration_trades']})"), + ('Zero Duration Trades', zero_duration_trades), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], From 916ece6a29fa140398bdc637b8bcf8194e0f7614 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:15:36 +0200 Subject: [PATCH 457/460] More realistic testcase for results --- tests/optimize/test_optimize_reports.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e0ff2f219..575bb9092 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -28,13 +28,10 @@ def test_text_table_bt_results(): results = pd.DataFrame( { - 'pair': ['ETH/BTC', 'ETH/BTC'], - 'profit_ratio': [0.1, 0.2], - 'profit_abs': [0.2, 0.4], - 'trade_duration': [10, 30], - 'wins': [2, 0], - 'draws': [0, 0], - 'losses': [0, 0] + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_ratio': [0.1, 0.2, -0.05], + 'profit_abs': [0.2, 0.4, -0.1], + 'trade_duration': [10, 30, 20], } ) @@ -43,10 +40,10 @@ def test_text_table_bt_results(): ' Avg Duration | Win Draw Loss Win% |\n' '|---------+--------+----------------+----------------+------------------+----------------+' '----------------+-------------------------|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' - ' 0:20:00 | 2 0 0 100 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 15.00 |' - ' 0:20:00 | 2 0 0 100 |' + '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' + ' 0:20:00 | 2 0 1 66.7 |\n' + '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' + ' 0:20:00 | 2 0 1 66.7 |' ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', From 7f125315b059cc529750415945366474efa5ac7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:36:02 +0200 Subject: [PATCH 458/460] Track Rejected Trades closes #3423 --- freqtrade/optimize/backtesting.py | 26 +++++++++++++++++++++----- freqtrade/optimize/optimize_reports.py | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aff09921c..642ed2b76 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -177,6 +177,7 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + self.rejected_trades = 0 self.dataprovider.clear_cache() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: @@ -336,6 +337,17 @@ class Backtesting: trades.append(trade1) return trades + def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: + if max_open_trades <= 0: + # Always allow trades when max_open_trades is enabled. + return True + if open_trade_count < max_open_trades: + + return True + # Rejected trade + self.rejected_trades += 1 + return False + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -397,11 +409,14 @@ class Backtesting: # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row - if ((position_stacking or len(open_trades[pair]) == 0) - and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) - and tmp != end_date - and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 - and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + if ( + (position_stacking or len(open_trades[pair]) == 0) + and self.trade_slot_available(max_open_trades, open_trade_count_start) + and tmp != end_date + and row[BUY_IDX] == 1 + and row[SELL_IDX] != 1 + and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) + ): trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades @@ -439,6 +454,7 @@ class Backtesting: 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), + 'rejected': self.rejected_trades, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ce6758210..ddccfd1dc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -355,6 +355,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], + 'rejected_signals': content['rejected'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -561,6 +562,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " From a39860e0de12e1d3c4590b235dc07c74460e7210 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:46:51 +0200 Subject: [PATCH 459/460] Add tests for rejected signals --- docs/backtesting.md | 3 +++ freqtrade/optimize/backtesting.py | 9 +++------ freqtrade/optimize/optimize_reports.py | 5 ++--- tests/optimize/test_backtesting.py | 3 +++ tests/optimize/test_hyperopt.py | 2 ++ tests/optimize/test_optimize_reports.py | 2 ++ 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e720206bb..2027c2079 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -298,6 +298,7 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -386,6 +387,7 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -416,6 +418,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results. +- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 642ed2b76..cbc0995aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -338,11 +338,8 @@ class Backtesting: return trades def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: - if max_open_trades <= 0: - # Always allow trades when max_open_trades is enabled. - return True - if open_trade_count < max_open_trades: - + # Always allow trades when max_open_trades is enabled. + if max_open_trades <= 0 or open_trade_count < max_open_trades: return True # Rejected trade self.rejected_trades += 1 @@ -454,7 +451,7 @@ class Backtesting: 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), - 'rejected': self.rejected_trades, + 'rejected_signals': self.rejected_trades, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ddccfd1dc..090af4a85 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -355,7 +355,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], - 'rejected_signals': content['rejected'], + 'rejected_signals': content['rejected_signals'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -562,8 +562,6 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), - ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), - ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), @@ -582,6 +580,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Zero Duration Trades', zero_duration_trades), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 03a65b159..632d458ce 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -831,6 +831,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', @@ -938,12 +939,14 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'results': result1, 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, }, { 'results': result2, 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, } ]) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b80ede734..d7eb8bf67 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -441,6 +441,7 @@ def test_hyperopt_format_results(hyperopt): 'config': hyperopt.config, 'locks': [], 'final_balance': 0.02, + 'rejected_signals': 2, 'backtest_start_time': 1619718665, 'backtest_end_time': 1619718665, } @@ -593,6 +594,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: }), 'config': hyperopt_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, } diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 575bb9092..f9dac3397 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -79,6 +79,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -126,6 +127,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } From 3f956441fcb4c6d011c1ac95c8f9e4fb249fb235 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 15:53:54 +0200 Subject: [PATCH 460/460] Properly format % of zero_duration_trades --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ce6758210..a76bb2e3a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -540,7 +540,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: if 'zero_duration_trades' in strat_results: zero_duration_trades_per = \ 100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades'] - zero_duration_trades = f'{zero_duration_trades_per}% ' \ + zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \ f'({strat_results["zero_duration_trades"]})' metrics = [