diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 268e3eeef..e072e12cb 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -7,6 +7,7 @@ from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file from freqtrade.exceptions import OperationalException +from freqtrade.optimize.optimize_reports import show_backtest_result from freqtrade.state import RunMode @@ -125,6 +126,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] + + metrics = val['results_metrics'] + if 'strategy_name' in metrics: + show_backtest_result(metrics['strategy_name'], metrics, + metrics['stake_currency']) + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, header_str="Epoch details") @@ -132,11 +139,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: """ Filter our items from the list of hyperopt results + TODO: after 2021.5 remove all "legacy" mode queries. """ if filteroptions['only_best']: epochs = [x for x in epochs if x['is_best']] if filteroptions['only_profitable']: - epochs = [x for x in epochs if x['results_metrics']['profit'] > 0] + epochs = [x for x in epochs if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total', 0)) > 0] epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) @@ -153,34 +162,55 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: return epochs +def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int): + """ + Filter epochs with trade-counts > trades + """ + return [ + x for x in epochs + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades', 0) + ) > trade_count + ] + + def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_trades'] > 0: - epochs = [ - x for x in epochs - if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] - ] + epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) + if filteroptions['filter_max_trades'] > 0: epochs = [ x for x in epochs - if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] + if x['results_metrics'].get( + 'trade_count', x['results_metrics'].get('total_trades') + ) < filteroptions['filter_max_trades'] ] return epochs def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: + def get_duration_value(x): + # Duration in minutes ... + if 'duration' in x['results_metrics']: + return x['results_metrics']['duration'] + else: + # New mode + avg = x['results_metrics']['holding_avg'] + return avg.total_seconds() // 60 + if filteroptions['filter_min_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time'] + if get_duration_value(x) > filteroptions['filter_min_avg_time'] ] if filteroptions['filter_max_avg_time'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] + if get_duration_value(x) < filteroptions['filter_max_avg_time'] ] return epochs @@ -189,28 +219,36 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] > filteroptions['filter_min_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 + ) > filteroptions['filter_min_avg_profit'] ] if filteroptions['filter_max_avg_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['avg_profit'] < filteroptions['filter_max_avg_profit'] + if x['results_metrics'].get( + 'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100 + ) < filteroptions['filter_max_avg_profit'] ] if filteroptions['filter_min_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total_abs', 0) + ) > filteroptions['filter_min_total_profit'] ] if filteroptions['filter_max_total_profit'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [ x for x in epochs - if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] + if x['results_metrics'].get( + 'profit', x['results_metrics'].get('profit_total_abs', 0) + ) < filteroptions['filter_max_total_profit'] ] return epochs @@ -218,11 +256,11 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: if filteroptions['filter_min_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] if filteroptions['filter_max_objective'] is not None: - epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] diff --git a/freqtrade/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 361480329..639e9fb93 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,11 +4,10 @@ This module contains the hyperopt logic """ -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 +29,7 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -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) @@ -79,9 +86,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) @@ -140,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. @@ -176,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) @@ -208,47 +209,42 @@ 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: """ Used Optimize function. Called once per epoch to optimize whatever is configured. Keep this function as optimized as possible! """ - params_dict = self._get_params_dict(raw_params) - params_details = self._get_params_details(params_dict) + backtest_start_time = datetime.now(timezone.utc) + params_dict = self._get_params_dict(self.dimensions, raw_params) + # Apply parameters if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore self.custom_hyperopt.generate_roi_table(params_dict)) @@ -275,28 +271,40 @@ class Hyperopt: processed = load(self.data_pickle_file) - 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, + 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), - ) - return self._get_results_dict(backtesting_results, min_date, max_date, - params_dict, params_details, + backtest_end_time = datetime.now(timezone.utc) + bt_results.update({ + 'backtest_start_time': int(backtest_start_time.timestamp()), + 'backtest_end_time': int(backtest_end_time.timestamp()), + }) + + return self._get_results_dict(bt_results, self.min_date, self.max_date, + params_dict, processed=processed) def _get_results_dict(self, backtesting_results, min_date, max_date, - params_dict, params_details, processed: Dict[str, DataFrame]): - results_metrics = self._calculate_results_metrics(backtesting_results) - results_explanation = self._format_results_explanation_string(results_metrics) + params_dict, processed: Dict[str, DataFrame] + ) -> Dict[str, Any]: + params_details = self._get_params_details(params_dict) - trade_count = results_metrics['trade_count'] - total_profit = results_metrics['total_profit'] + strat_stats = generate_strategy_stats( + processed, self.backtesting.strategy.get_strategy_name(), + backtesting_results, min_date, max_date, market_change=0 + ) + results_explanation = HyperoptTools.format_results_explanation_string( + strat_stats, self.config['stake_currency']) + + not_optimized = self.backtesting.strategy.get_params_dict() + + trade_count = strat_stats['total_trades'] + total_profit = strat_stats['profit_total'] # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) @@ -304,50 +312,20 @@ class Hyperopt: # path. We do not want to optimize 'hodl' strategies. loss: float = MAX_LOSS if trade_count >= self.config['hyperopt_min_trades']: - loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, + 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, + 'params_not_optimized': not_optimized, + 'results_metrics': strat_stats, 'results_explanation': results_explanation, 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: - wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0]) - draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0]) - losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0]) - return { - 'trade_count': len(backtesting_results.index), - 'wins': wins, - 'draws': draws, - 'losses': losses, - 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", - 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0, - 'median_profit': backtesting_results['profit_ratio'].median() * 100.0, - 'total_profit': backtesting_results['profit_abs'].sum(), - 'profit': backtesting_results['profit_ratio'].sum() * 100.0, - 'duration': backtesting_results['trade_duration'].mean(), - } - - def _format_results_explanation_string(self, results_metrics: Dict) -> str: - """ - Return the formatted results explanation in a string - """ - stake_cur = self.config['stake_currency'] - return (f"{results_metrics['trade_count']:6d} trades. " - f"{results_metrics['wins']}/{results_metrics['draws']}" - f"/{results_metrics['losses']} Wins/Draws/Losses. " - f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " - f"Median profit {results_metrics['median_profit']: 6.2f}%. " - f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " - f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['duration']:5.1f} min." - ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') - def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( dimensions, @@ -370,6 +348,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) @@ -378,11 +358,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) @@ -400,7 +380,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/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index a223a68bb..f655582c4 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,9 +1,9 @@ import io +import locale import logging from collections import OrderedDict from pathlib import Path -from pprint import pformat from typing import Any, Dict, List import rapidjson @@ -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__) @@ -65,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: @@ -81,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:") @@ -108,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) @@ -122,29 +125,64 @@ class HyperoptTools(): (str(k), v) for k, v in space_params.items() ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" + result += f"minimal_roi = {minimal_roi_result}" elif space == 'trailing': for k, v in space_params.items(): - params_result += f'{k} = {v}\n' + result += f'{k} = {v}\n' else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + no_params = HyperoptTools._space_params(non_optimized, space, 5) - params_result = params_result.replace("\n", "\n ") - print(params_result) + result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}" + + result = result.replace("\n", "\n ") + print(result) @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d + d = params.get(space) + if d: + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + return {} + + @staticmethod + def _pprint(params, non_optimized, indent: int = 4): + """ + Pretty-print hyperopt results (based on 2 dicts - with add. comment) + """ + p = params.copy() + p.update(non_optimized) + result = '{\n' + + for k, param in p.items(): + result += " " * indent + f'"{k}": {param},' + if k in non_optimized: + result += " # value loaded from strategy" + result += "\n" + result += '}' + return result @staticmethod def is_best_loss(results, current_best_loss: float) -> bool: return results['loss'] < current_best_loss + @staticmethod + def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str: + """ + Return the formatted results explanation in a string + """ + return (f"{results_metrics['total_trades']:6d} trades. " + f"{results_metrics['wins']}/{results_metrics['draws']}" + f"/{results_metrics['losses']} Wins/Draws/Losses. " + f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. " + f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. " + f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} " + f"({results_metrics['profit_total'] * 100: 7.2f}\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 " ") + @@ -168,12 +206,27 @@ class HyperoptTools(): if 'results_metrics.winsdrawslosses' not in trials.columns: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.winsdrawslosses'] = 'N/A' + legacy_mode = True + + if 'results_metrics.total_trades' in trials: + legacy_mode = False + # New mode, using backtest result for metrics + trials['results_metrics.winsdrawslosses'] = trials.apply( + lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " + f"{x['results_metrics.losses']:>4}", axis=1) + trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', + 'results_metrics.winsdrawslosses', + 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', + 'results_metrics.profit_total', 'results_metrics.holding_avg', + 'loss', 'is_initial_point', 'is_best']] + else: + # Legacy mode + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', 'Total profit', 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] @@ -183,26 +236,28 @@ class HyperoptTools(): trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Trades'] = trials['Trades'].astype(str) - + perc_multi = 1 if legacy_mode else 100 trials['Epoch'] = trials['Epoch'].apply( lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) ) trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x * perc_multi:,.2f}%'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') ) trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" + if not isna(x) else "--".rjust(7, ' ') ) trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') ) + stake_currency = config['stake_currency'] trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + lambda x: '{} {}'.format( + round_coin_value(x['Total profit'], stake_currency), + '({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ') + ).rjust(25+len(stake_currency)) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), axis=1 ) trials = trials.drop(columns=['Total profit']) @@ -263,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] @@ -284,21 +349,23 @@ 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/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a80dc5d31..9a60c7c4b 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 @@ -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,13 +253,148 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, - 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) - if not winning_trades.empty else timedelta()), - 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) - if not losing_trades.empty else timedelta()), } +def generate_strategy_stats(btdata: Dict[str, DataFrame], + strategy: str, + content: Dict[str, Any], + min_date: 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) + trade_stats = generate_trading_stats(results) + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 + results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 + + backtest_days = (max_date - min_date).days + strat_stats = { + 'trades': results.to_dict(orient='records'), + 'locks': [lock.to_json() for lock in content['locks']], + 'best_pair': best_pair, + 'worst_pair': worst_pair, + 'results_per_pair': pair_results, + 'sell_reason_summary': sell_reason_stats, + 'left_open_trades': left_open_results, + 'total_trades': len(results), + 'total_volume': float(results['stake_amount'].sum()), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, + 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, + 'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0, + 'profit_total': results['profit_abs'].sum() / starting_balance, + 'profit_total_abs': results['profit_abs'].sum(), + 'backtest_start': min_date.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, + **trade_stats + } + + try: + max_drawdown, _, _, _, _ = calculate_max_drawdown( + results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') + strat_stats.update({ + 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, + 'drawdown_start': drawdown_start, + '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,132 +405,17 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date - :return: - Dictionary containing results per strategy and a stratgy summary. + :return: Dictionary containing results per strategy and a stratgy summary. """ result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') for strategy, content in all_results.items(): - results: Dict[str, DataFrame] = content['results'] - if not isinstance(results, DataFrame): - continue - config = content['config'] - max_open_trades = min(config['max_open_trades'], len(btdata.keys())) - starting_balance = config['dry_run_wallet'] - stake_currency = config['stake_currency'] - - pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) - sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, - results=results) - left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results.loc[results['is_open']], - skip_nan=True) - daily_stats = generate_daily_stats(results) - best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], - key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], - key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 - results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 - - backtest_days = (max_date - min_date).days - strat_stats = { - 'trades': results.to_dict(orient='records'), - 'locks': [lock.to_json() for lock in content['locks']], - 'best_pair': best_pair, - 'worst_pair': worst_pair, - 'results_per_pair': pair_results, - 'sell_reason_summary': sell_reason_stats, - 'left_open_trades': left_open_results, - 'total_trades': len(results), - 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, - 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_abs'].sum() / starting_balance, - 'profit_total_abs': results['profit_abs'].sum(), - 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.int_timestamp * 1000, - 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.int_timestamp * 1000, - 'backtest_days': backtest_days, - - 'backtest_run_start_ts': content['backtest_start_time'], - 'backtest_run_end_ts': content['backtest_end_time'], - - 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, - 'market_change': market_change, - 'pairlist': list(btdata.keys()), - 'stake_amount': config['stake_amount'], - 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), - 'starting_balance': starting_balance, - 'dry_run_wallet': starting_balance, - 'final_balance': content['final_balance'], - 'max_open_trades': max_open_trades, - 'max_open_trades_setting': (config['max_open_trades'] - if config['max_open_trades'] != float('inf') else -1), - 'timeframe': config['timeframe'], - 'timerange': config.get('timerange', ''), - 'enable_protections': config.get('enable_protections', False), - 'strategy_name': strategy, - # Parameters relevant for backtesting - 'stoploss': config['stoploss'], - 'trailing_stop': config.get('trailing_stop', False), - 'trailing_stop_positive': config.get('trailing_stop_positive'), - 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), - 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), - 'use_custom_stoploss': config.get('use_custom_stoploss', False), - 'minimal_roi': config['minimal_roi'], - 'use_sell_signal': config['ask_strategy']['use_sell_signal'], - 'sell_profit_only': config['ask_strategy']['sell_profit_only'], - 'sell_profit_offset': config['ask_strategy']['sell_profit_offset'], - 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], - **daily_stats, - } + strat_stats = generate_strategy_stats(btdata, strategy, content, + min_date, max_date, market_change=market_change) result['strategy'][strategy] = strat_stats - try: - max_drawdown, _, _, _, _ = calculate_max_drawdown( - results, value_col='profit_ratio') - drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( - results, value_col='profit_abs') - strat_stats.update({ - 'max_drawdown': max_drawdown, - 'max_drawdown_abs': drawdown_abs, - 'drawdown_start': drawdown_start, - 'drawdown_start_ts': drawdown_start.timestamp() * 1000, - 'drawdown_end': drawdown_end, - 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, - }) - - csum_min, csum_max = calculate_csum(results, starting_balance) - strat_stats.update({ - 'csum_min': csum_min, - 'csum_max': csum_max - }) - - except ValueError: - strat_stats.update({ - 'max_drawdown': 0.0, - 'max_drawdown_abs': 0.0, - 'max_drawdown_low': 0.0, - 'max_drawdown_high': 0.0, - 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_start_ts': 0, - 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), - 'drawdown_end_ts': 0, - 'csum_min': 0, - 'csum_max': 0 - }) - - strategy_results = generate_strategy_metrics(all_results=all_results) + strategy_results = generate_strategy_comparison(all_results=all_results) result['strategy_comparison'] = strategy_results @@ -522,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 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/commands/test_commands.py b/tests/commands/test_commands.py index d86bced5d..4d3937d87 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -918,242 +918,244 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): captured.out) -def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, + saved_hyperopt_results_legacy): + for _ in (saved_hyperopt_results, saved_hyperopt_results_legacy): + mocker.patch( + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', + MagicMock(return_value=saved_hyperopt_results_legacy) + ) + + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", + " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--best", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 5/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-color", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", + "Sell hyperspace params", "ROI table", "Stoploss"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--max-trades", "20", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-profit", "0.11", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-profit", "0.10", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", + " 11/12"]) + assert all(x not in captured.out + for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--min-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--profitable", + "--no-details", + "--no-color", + "--min-avg-time", "2000", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", + " 8/12", " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--max-avg-time", "1500", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 2/12", " 6/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" + " 9/12", " 10/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--no-color", + "--export-csv", "test_file.csv", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + log_has("CSV file created: test_file.csv", caplog) + f = Path("test_file.csv") + assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() + assert f.is_file() + f.unlink() + + +def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) - ) - - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", - " 6/12", " 7/12", " 8/12", " 9/12", " 10/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--best", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 5/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-color", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", - "Sell hyperspace params", "ROI table", "Stoploss"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--max-trades", "20", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-profit", "0.11", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-profit", "0.10", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", - " 11/12"]) - assert all(x not in captured.out - for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-total-profit", "0.4", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--min-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--max-objective", "0.1", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", - " 9/12", " 11/12"]) - assert all(x not in captured.out - for x in [" 4/12", " 10/12", " 12/12"]) - args = [ - "hyperopt-list", - "--profitable", - "--no-details", - "--no-color", - "--min-avg-time", "2000", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 10/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", - " 8/12", " 9/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--max-avg-time", "1500", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - assert all(x in captured.out - for x in [" 2/12", " 6/12"]) - assert all(x not in captured.out - for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" - " 9/12", " 10/12", " 11/12", " 12/12"]) - args = [ - "hyperopt-list", - "--no-details", - "--no-color", - "--export-csv", "test_file.csv", - ] - pargs = get_args(args) - pargs['config'] = None - start_hyperopt_list(pargs) - captured = capsys.readouterr() - log_has("CSV file created: test_file.csv", caplog) - f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() - assert f.is_file() - f.unlink() - - -def test_hyperopt_show(mocker, capsys, hyperopt_results): - mocker.patch( - 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', - MagicMock(return_value=hyperopt_results) + MagicMock(return_value=saved_hyperopt_results) ) args = [ diff --git a/tests/conftest.py b/tests/conftest.py index 788586134..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 hyperopt_results(): +def saved_hyperopt_results_legacy(): return [ { 'loss': 0.4366182531161519, @@ -1907,3 +1907,136 @@ def hyperopt_results(): 'is_best': False } ] + + +@pytest.fixture +def saved_hyperopt_results(): + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'holding_avg': timedelta(minutes=3930.0)}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': { + 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + # New Hyperopt mode! + 'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] diff --git a/tests/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 00114be5b..5dfc9dbc3 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): @@ -802,8 +804,19 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) - backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS)) + backtestmock = MagicMock(return_value={ + 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) @@ -817,7 +830,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) @@ -865,41 +878,58 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): - + default_conf['ask_strategy'].update({ + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + }) patch_exchange(mocker) + result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', ], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', ], utc=True), + 'trade_duration': [235, 40], + 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], + 'profit_ratio': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', + '2018-01-30 03:30:00', + '2018-01-30 05:30:00'], utc=True + ), + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', + '2018-01-30 05:35:00', + '2018-01-30 08:30:00'], utc=True), + 'trade_duration': [47, 40, 20], + 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }) backtestmock = MagicMock(side_effect=[ - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], - 'profit_ratio': [0.0, 0.0], - 'profit_abs': [0.0, 0.0], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', ], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', ], utc=True), - 'trade_duration': [235, 40], - 'is_open': [False, False], - 'stake_amount': [0.01, 0.01], - 'open_rate': [0.104445, 0.10302485], - 'close_rate': [0.104969, 0.103541], - 'sell_reason': [SellType.ROI, SellType.ROI] - }), - pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], - 'profit_ratio': [0.03, 0.01, 0.1], - 'profit_abs': [0.01, 0.02, 0.2], - 'open_date': pd.to_datetime(['2018-01-29 18:40:00', - '2018-01-30 03:30:00', - '2018-01-30 05:30:00'], utc=True - ), - 'close_date': pd.to_datetime(['2018-01-29 20:45:00', - '2018-01-30 05:35:00', - '2018-01-30 08:30:00'], utc=True), - 'trade_duration': [47, 40, 20], - 'is_open': [False, False, False], - 'stake_amount': [0.01, 0.01, 0.01], - 'open_rate': [0.104445, 0.10302485, 0.122541], - 'close_rate': [0.104969, 0.103541, 0.123541], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - }), + { + 'results': result1, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + }, + { + 'results': result2, + 'config': default_conf, + 'locks': [], + 'final_balance': 1000, + } ]) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index cd405c52f..774dd35a4 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) - results_explanation = hyperopt._format_results_explanation_string(results_metrics) - total_profit = results_metrics['total_profit'] +def test_hyperopt_format_results(hyperopt): + + bt_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt.config, + 'locks': [], + 'final_balance': 0.02, + 'backtest_start_time': 1619718665, + 'backtest_end_time': 1619718665, + } + results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, + Arrow(2017, 11, 14, 19, 32, 00), + Arrow(2017, 12, 14, 19, 32, 00), market_change=0) + + results_explanation = HyperoptTools.format_results_explanation_string(results_metrics, 'BTC') + total_profit = results_metrics['profit_total_abs'] results = { 'loss': 0.0, @@ -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,37 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'hyperopt_min_trades': 1, }) - trades = [ - ('TRX/BTC', 0.023117, 0.000233, 100) - ] - labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] - backtest_result = pd.DataFrame.from_records(trades, columns=labels) + backtest_result = { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': hyperopt_conf, + 'locks': [], + 'final_balance': 1000, + } - mocker.patch( - 'freqtrade.optimize.hyperopt.Backtesting.backtest', - MagicMock(return_value=backtest_result) - ) - mocker.patch( - 'freqtrade.optimize.hyperopt.get_timerange', - MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) - ) + mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) + mocker.patch('freqtrade.optimize.hyperopt.get_timerange', + return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) patch_exchange(mocker) - mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { 'adx-value': 0, @@ -626,11 +654,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,21 +688,16 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, - 'results_metrics': {'avg_profit': 2.3117, - 'draws': 0, - 'duration': 100.0, - 'losses': 0, - 'winsdrawslosses': ' 1 0 0', - 'median_profit': 2.3117, - 'profit': 2.3117, - 'total_profit': 0.000233, - 'trade_count': 1, - 'wins': 1}, - 'total_profit': 0.00023300 + 'params_not_optimized': {'buy': {}, 'sell': {}}, + 'results_metrics': ANY, + 'total_profit': 3.1e-08 } hyperopt = Hyperopt(hyperopt_conf) - hyperopt.dimensions = hyperopt.hyperopt_space() + hyperopt.min_date = Arrow(2017, 12, 10) + hyperopt.max_date = Arrow(2017, 12, 13) + hyperopt.init_spaces() + hyperopt.dimensions = hyperopt.dimensions generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8119c732b..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_metrics, store_backtest_stats, + generate_strategy_comparison, + generate_trading_stats, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -226,8 +227,6 @@ def test_generate_daily_stats(testdatadir): assert res['winning_days'] == 14 assert res['draw_days'] == 4 assert res['losing_days'] == 3 - assert res['winner_holding_avg'] == timedelta(seconds=1440) - assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) # Select empty dataframe! res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) @@ -238,6 +237,23 @@ def test_generate_daily_stats(testdatadir): assert res['losing_days'] == 0 +def test_generate_trading_stats(testdatadir): + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_data(filename) + res = generate_trading_stats(bt_data) + assert isinstance(res, dict) + assert res['winner_holding_avg'] == timedelta(seconds=1440) + assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) + assert 'wins' in res + assert 'losses' in res + assert 'draws' in res + + # Select empty dataframe! + res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) + assert res['wins'] == 0 + assert res['losses'] == 0 + + def test_text_table_sell_reason(): results = pd.DataFrame( @@ -345,7 +361,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 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')]