Merge pull request #4848 from freqtrade/hyperopt_btresults
Hyperopt store backtest-outcome
This commit is contained in:
		| @@ -7,6 +7,7 @@ from colorama import init as colorama_init | |||||||
| from freqtrade.configuration import setup_utils_configuration | from freqtrade.configuration import setup_utils_configuration | ||||||
| from freqtrade.data.btanalysis import get_latest_hyperopt_file | from freqtrade.data.btanalysis import get_latest_hyperopt_file | ||||||
| from freqtrade.exceptions import OperationalException | from freqtrade.exceptions import OperationalException | ||||||
|  | from freqtrade.optimize.optimize_reports import show_backtest_result | ||||||
| from freqtrade.state import RunMode | from freqtrade.state import RunMode | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -125,6 +126,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: | |||||||
|  |  | ||||||
|     if epochs: |     if epochs: | ||||||
|         val = epochs[n] |         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, |         HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, | ||||||
|                                           header_str="Epoch details") |                                           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: | def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: | ||||||
|     """ |     """ | ||||||
|     Filter our items from the list of hyperopt results |     Filter our items from the list of hyperopt results | ||||||
|  |     TODO: after 2021.5 remove all "legacy" mode queries. | ||||||
|     """ |     """ | ||||||
|     if filteroptions['only_best']: |     if filteroptions['only_best']: | ||||||
|         epochs = [x for x in epochs if x['is_best']] |         epochs = [x for x in epochs if x['is_best']] | ||||||
|     if filteroptions['only_profitable']: |     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) |     epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) | ||||||
|  |  | ||||||
| @@ -153,34 +162,55 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: | |||||||
|     return epochs |     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: | def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: | ||||||
|  |  | ||||||
|     if filteroptions['filter_min_trades'] > 0: |     if filteroptions['filter_min_trades'] > 0: | ||||||
|         epochs = [ |         epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades']) | ||||||
|             x for x in epochs |  | ||||||
|             if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] |  | ||||||
|         ] |  | ||||||
|     if filteroptions['filter_max_trades'] > 0: |     if filteroptions['filter_max_trades'] > 0: | ||||||
|         epochs = [ |         epochs = [ | ||||||
|             x for x in 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 |     return epochs | ||||||
|  |  | ||||||
|  |  | ||||||
| def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: | 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: |     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 = [ |         epochs = [ | ||||||
|             x for x in 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: |     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 = [ |         epochs = [ | ||||||
|             x for x in 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 |     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: | def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: | ||||||
|  |  | ||||||
|     if filteroptions['filter_min_avg_profit'] is not None: |     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 = [ |         epochs = [ | ||||||
|             x for x in 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: |     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 = [ |         epochs = [ | ||||||
|             x for x in 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: |     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 = [ |         epochs = [ | ||||||
|             x for x in 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: |     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 = [ |         epochs = [ | ||||||
|             x for x in 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 |     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: | def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: | ||||||
|  |  | ||||||
|     if filteroptions['filter_min_objective'] is not None: |     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']] |         epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] | ||||||
|     if filteroptions['filter_max_objective'] is not None: |     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']] |         epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -330,7 +330,7 @@ class Backtesting: | |||||||
|     def backtest(self, processed: Dict, |     def backtest(self, processed: Dict, | ||||||
|                  start_date: datetime, end_date: datetime, |                  start_date: datetime, end_date: datetime, | ||||||
|                  max_open_trades: int = 0, position_stacking: bool = False, |                  max_open_trades: int = 0, position_stacking: bool = False, | ||||||
|                  enable_protections: bool = False) -> DataFrame: |                  enable_protections: bool = False) -> Dict[str, Any]: | ||||||
|         """ |         """ | ||||||
|         Implement backtesting functionality |         Implement backtesting functionality | ||||||
|  |  | ||||||
| @@ -417,7 +417,13 @@ class Backtesting: | |||||||
|         trades += self.handle_left_open(open_trades, data=data) |         trades += self.handle_left_open(open_trades, data=data) | ||||||
|         self.wallets.update() |         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): |     def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): | ||||||
|         logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) |         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), |             enable_protections=self.config.get('enable_protections', False), | ||||||
|         ) |         ) | ||||||
|         backtest_end_time = datetime.now(timezone.utc) |         backtest_end_time = datetime.now(timezone.utc) | ||||||
|         self.all_results[self.strategy.get_strategy_name()] = { |         results.update({ | ||||||
|             'results': results, |  | ||||||
|             'config': self.strategy.config, |  | ||||||
|             'locks': PairLocks.get_all_locks(), |  | ||||||
|             'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), |  | ||||||
|             'backtest_start_time': int(backtest_start_time.timestamp()), |             'backtest_start_time': int(backtest_start_time.timestamp()), | ||||||
|             'backtest_end_time': int(backtest_end_time.timestamp()), |             'backtest_end_time': int(backtest_end_time.timestamp()), | ||||||
|         } |         }) | ||||||
|  |         self.all_results[self.strategy.get_strategy_name()] = results | ||||||
|  |  | ||||||
|         return min_date, max_date |         return min_date, max_date | ||||||
|  |  | ||||||
|     def start(self) -> None: |     def start(self) -> None: | ||||||
|   | |||||||
| @@ -4,11 +4,10 @@ | |||||||
| This module contains the hyperopt logic | This module contains the hyperopt logic | ||||||
| """ | """ | ||||||
|  |  | ||||||
| import locale |  | ||||||
| import logging | import logging | ||||||
| import random | import random | ||||||
| import warnings | import warnings | ||||||
| from datetime import datetime | from datetime import datetime, timezone | ||||||
| from math import ceil | from math import ceil | ||||||
| from operator import itemgetter | from operator import itemgetter | ||||||
| from pathlib import Path | 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_interface import IHyperOpt  # noqa: F401 | ||||||
| from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss  # noqa: F401 | from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss  # noqa: F401 | ||||||
| from freqtrade.optimize.hyperopt_tools import HyperoptTools | 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.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver | ||||||
| from freqtrade.strategy import IStrategy | from freqtrade.strategy import IStrategy | ||||||
|  |  | ||||||
| @@ -65,6 +65,13 @@ class Hyperopt: | |||||||
|     custom_hyperopt: IHyperOpt |     custom_hyperopt: IHyperOpt | ||||||
|  |  | ||||||
|     def __init__(self, config: Dict[str, Any]) -> None: |     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.config = config | ||||||
|  |  | ||||||
|         self.backtesting = Backtesting(self.config) |         self.backtesting = Backtesting(self.config) | ||||||
| @@ -79,9 +86,8 @@ class Hyperopt: | |||||||
|         self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function |         self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function | ||||||
|         time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") |         time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") | ||||||
|         strategy = str(self.config['strategy']) |         strategy = str(self.config['strategy']) | ||||||
|         self.results_file = (self.config['user_data_dir'] / |         self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' / | ||||||
|                              'hyperopt_results' / |                                    f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') | ||||||
|                              f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') |  | ||||||
|         self.data_pickle_file = (self.config['user_data_dir'] / |         self.data_pickle_file = (self.config['user_data_dir'] / | ||||||
|                                  'hyperopt_results' / 'hyperopt_tickerdata.pkl') |                                  'hyperopt_results' / 'hyperopt_tickerdata.pkl') | ||||||
|         self.total_epochs = config.get('epochs', 0) |         self.total_epochs = config.get('epochs', 0) | ||||||
| @@ -140,9 +146,7 @@ class Hyperopt: | |||||||
|                 logger.info(f"Removing `{p}`.") |                 logger.info(f"Removing `{p}`.") | ||||||
|                 p.unlink() |                 p.unlink() | ||||||
|  |  | ||||||
|     def _get_params_dict(self, raw_params: List[Any]) -> Dict: |     def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: | ||||||
|  |  | ||||||
|         dimensions: List[Dimension] = self.dimensions |  | ||||||
|  |  | ||||||
|         # Ensure the number of dimensions match |         # Ensure the number of dimensions match | ||||||
|         # the number of parameters in the list. |         # the number of parameters in the list. | ||||||
| @@ -176,16 +180,13 @@ class Hyperopt: | |||||||
|         result: Dict = {} |         result: Dict = {} | ||||||
|  |  | ||||||
|         if HyperoptTools.has_space(self.config, 'buy'): |         if HyperoptTools.has_space(self.config, 'buy'): | ||||||
|             result['buy'] = {p.name: params.get(p.name) |             result['buy'] = {p.name: params.get(p.name) for p in self.buy_space} | ||||||
|                              for p in self.hyperopt_space('buy')} |  | ||||||
|         if HyperoptTools.has_space(self.config, 'sell'): |         if HyperoptTools.has_space(self.config, 'sell'): | ||||||
|             result['sell'] = {p.name: params.get(p.name) |             result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} | ||||||
|                               for p in self.hyperopt_space('sell')} |  | ||||||
|         if HyperoptTools.has_space(self.config, 'roi'): |         if HyperoptTools.has_space(self.config, 'roi'): | ||||||
|             result['roi'] = self.custom_hyperopt.generate_roi_table(params) |             result['roi'] = self.custom_hyperopt.generate_roi_table(params) | ||||||
|         if HyperoptTools.has_space(self.config, 'stoploss'): |         if HyperoptTools.has_space(self.config, 'stoploss'): | ||||||
|             result['stoploss'] = {p.name: params.get(p.name) |             result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} | ||||||
|                                   for p in self.hyperopt_space('stoploss')} |  | ||||||
|         if HyperoptTools.has_space(self.config, 'trailing'): |         if HyperoptTools.has_space(self.config, 'trailing'): | ||||||
|             result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) |             result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) | ||||||
|  |  | ||||||
| @@ -208,47 +209,42 @@ class Hyperopt: | |||||||
|             ) |             ) | ||||||
|             self.hyperopt_table_header = 2 |             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. |         Assign the dimensions in the hyperoptimization space. | ||||||
|         :param space: Defines hyperspace to return dimensions for. |  | ||||||
|         If None, then the self.has_space() will be used to return dimensions |  | ||||||
|         for all hyperspaces used. |  | ||||||
|         """ |         """ | ||||||
|         spaces: List[Dimension] = [] |  | ||||||
|  |  | ||||||
|         if space == 'buy' or (space is None and HyperoptTools.has_space(self.config, 'buy')): |         if HyperoptTools.has_space(self.config, 'buy'): | ||||||
|             logger.debug("Hyperopt has 'buy' space") |             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") |             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") |             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 |         if HyperoptTools.has_space(self.config, 'stoploss'): | ||||||
|                                    and HyperoptTools.has_space(self.config, 'stoploss')): |  | ||||||
|             logger.debug("Hyperopt has 'stoploss' space") |             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 |         if HyperoptTools.has_space(self.config, 'trailing'): | ||||||
|                                    and HyperoptTools.has_space(self.config, 'trailing')): |  | ||||||
|             logger.debug("Hyperopt has 'trailing' space") |             logger.debug("Hyperopt has 'trailing' space") | ||||||
|             spaces += self.custom_hyperopt.trailing_space() |             self.trailing_space = self.custom_hyperopt.trailing_space() | ||||||
|  |         self.dimensions = (self.buy_space + self.sell_space + self.roi_space + | ||||||
|         return spaces |                            self.stoploss_space + self.trailing_space) | ||||||
|  |  | ||||||
|     def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: |     def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: | ||||||
|         """ |         """ | ||||||
|         Used Optimize function. Called once per epoch to optimize whatever is configured. |         Used Optimize function. Called once per epoch to optimize whatever is configured. | ||||||
|         Keep this function as optimized as possible! |         Keep this function as optimized as possible! | ||||||
|         """ |         """ | ||||||
|         params_dict = self._get_params_dict(raw_params) |         backtest_start_time = datetime.now(timezone.utc) | ||||||
|         params_details = self._get_params_details(params_dict) |         params_dict = self._get_params_dict(self.dimensions, raw_params) | ||||||
|  |  | ||||||
|  |         # Apply parameters | ||||||
|         if HyperoptTools.has_space(self.config, 'roi'): |         if HyperoptTools.has_space(self.config, 'roi'): | ||||||
|             self.backtesting.strategy.minimal_roi = (  # type: ignore |             self.backtesting.strategy.minimal_roi = (  # type: ignore | ||||||
|                 self.custom_hyperopt.generate_roi_table(params_dict)) |                 self.custom_hyperopt.generate_roi_table(params_dict)) | ||||||
| @@ -275,28 +271,40 @@ class Hyperopt: | |||||||
|  |  | ||||||
|         processed = load(self.data_pickle_file) |         processed = load(self.data_pickle_file) | ||||||
|  |  | ||||||
|         min_date, max_date = get_timerange(processed) |         bt_results = self.backtesting.backtest( | ||||||
|  |  | ||||||
|         backtesting_results = self.backtesting.backtest( |  | ||||||
|             processed=processed, |             processed=processed, | ||||||
|             start_date=min_date.datetime, |             start_date=self.min_date.datetime, | ||||||
|             end_date=max_date.datetime, |             end_date=self.max_date.datetime, | ||||||
|             max_open_trades=self.max_open_trades, |             max_open_trades=self.max_open_trades, | ||||||
|             position_stacking=self.position_stacking, |             position_stacking=self.position_stacking, | ||||||
|             enable_protections=self.config.get('enable_protections', False), |             enable_protections=self.config.get('enable_protections', False), | ||||||
|  |  | ||||||
|         ) |         ) | ||||||
|         return self._get_results_dict(backtesting_results, min_date, max_date, |         backtest_end_time = datetime.now(timezone.utc) | ||||||
|                                       params_dict, params_details, |         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) |                                       processed=processed) | ||||||
|  |  | ||||||
|     def _get_results_dict(self, backtesting_results, min_date, max_date, |     def _get_results_dict(self, backtesting_results, min_date, max_date, | ||||||
|                           params_dict, params_details, processed: Dict[str, DataFrame]): |                           params_dict, processed: Dict[str, DataFrame] | ||||||
|         results_metrics = self._calculate_results_metrics(backtesting_results) |                           ) -> Dict[str, Any]: | ||||||
|         results_explanation = self._format_results_explanation_string(results_metrics) |         params_details = self._get_params_details(params_dict) | ||||||
|  |  | ||||||
|         trade_count = results_metrics['trade_count'] |         strat_stats = generate_strategy_stats( | ||||||
|         total_profit = results_metrics['total_profit'] |             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 |         # If this evaluation contains too short amount of trades to be | ||||||
|         # interesting -- consider it as 'bad' (assigned max. loss value) |         # interesting -- consider it as 'bad' (assigned max. loss value) | ||||||
| @@ -304,50 +312,20 @@ class Hyperopt: | |||||||
|         # path. We do not want to optimize 'hodl' strategies. |         # path. We do not want to optimize 'hodl' strategies. | ||||||
|         loss: float = MAX_LOSS |         loss: float = MAX_LOSS | ||||||
|         if trade_count >= self.config['hyperopt_min_trades']: |         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, |                                        min_date=min_date.datetime, max_date=max_date.datetime, | ||||||
|                                        config=self.config, processed=processed) |                                        config=self.config, processed=processed) | ||||||
|         return { |         return { | ||||||
|             'loss': loss, |             'loss': loss, | ||||||
|             'params_dict': params_dict, |             'params_dict': params_dict, | ||||||
|             'params_details': params_details, |             'params_details': params_details, | ||||||
|             'results_metrics': results_metrics, |             'params_not_optimized': not_optimized, | ||||||
|  |             'results_metrics': strat_stats, | ||||||
|             'results_explanation': results_explanation, |             'results_explanation': results_explanation, | ||||||
|             'total_profit': total_profit, |             '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: |     def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: | ||||||
|         return Optimizer( |         return Optimizer( | ||||||
|             dimensions, |             dimensions, | ||||||
| @@ -370,6 +348,8 @@ class Hyperopt: | |||||||
|         self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) |         self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) | ||||||
|         logger.info(f"Using optimizer random state: {self.random_state}") |         logger.info(f"Using optimizer random state: {self.random_state}") | ||||||
|         self.hyperopt_table_header = -1 |         self.hyperopt_table_header = -1 | ||||||
|  |         # Initialize spaces ... | ||||||
|  |         self.init_spaces() | ||||||
|         data, timerange = self.backtesting.load_bt_data() |         data, timerange = self.backtesting.load_bt_data() | ||||||
|         logger.info("Dataload complete. Calculating indicators") |         logger.info("Dataload complete. Calculating indicators") | ||||||
|         preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) |         preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) | ||||||
| @@ -378,11 +358,11 @@ class Hyperopt: | |||||||
|         for pair, df in preprocessed.items(): |         for pair, df in preprocessed.items(): | ||||||
|             preprocessed[pair] = trim_dataframe(df, timerange, |             preprocessed[pair] = trim_dataframe(df, timerange, | ||||||
|                                                 startup_candles=self.backtesting.required_startup) |                                                 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)} ' |         logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' | ||||||
|                     f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' |                     f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' | ||||||
|                     f'({(max_date - min_date).days} days)..') |                     f'({(self.max_date - self.min_date).days} days)..') | ||||||
|  |  | ||||||
|         dump(preprocessed, self.data_pickle_file) |         dump(preprocessed, self.data_pickle_file) | ||||||
|  |  | ||||||
| @@ -400,7 +380,6 @@ class Hyperopt: | |||||||
|         config_jobs = self.config.get('hyperopt_jobs', -1) |         config_jobs = self.config.get('hyperopt_jobs', -1) | ||||||
|         logger.info(f'Number of parallel jobs set as: {config_jobs}') |         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) |         self.opt = self.get_optimizer(self.dimensions, config_jobs) | ||||||
|  |  | ||||||
|         if self.print_colorized: |         if self.print_colorized: | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
|  |  | ||||||
| import io | import io | ||||||
|  | import locale | ||||||
| import logging | import logging | ||||||
| from collections import OrderedDict | from collections import OrderedDict | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from pprint import pformat |  | ||||||
| from typing import Any, Dict, List | from typing import Any, Dict, List | ||||||
|  |  | ||||||
| import rapidjson | import rapidjson | ||||||
| @@ -12,7 +12,7 @@ from colorama import Fore, Style | |||||||
| from pandas import isna, json_normalize | from pandas import isna, json_normalize | ||||||
|  |  | ||||||
| from freqtrade.exceptions import OperationalException | from freqtrade.exceptions import OperationalException | ||||||
| from freqtrade.misc import round_dict | from freqtrade.misc import round_coin_value, round_dict | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -65,6 +65,7 @@ class HyperoptTools(): | |||||||
|         Display details of the hyperopt result |         Display details of the hyperopt result | ||||||
|         """ |         """ | ||||||
|         params = results.get('params_details', {}) |         params = results.get('params_details', {}) | ||||||
|  |         non_optimized = results.get('params_not_optimized', {}) | ||||||
|  |  | ||||||
|         # Default header string |         # Default header string | ||||||
|         if header_str is None: |         if header_str is None: | ||||||
| @@ -81,8 +82,10 @@ class HyperoptTools(): | |||||||
|             print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) |             print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) | ||||||
|  |  | ||||||
|         else: |         else: | ||||||
|             HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") |             HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", | ||||||
|             HyperoptTools._params_pretty_print(params, 'sell', "Sell 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, 'roi', "ROI table:") | ||||||
|             HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") |             HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") | ||||||
|             HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") |             HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") | ||||||
| @@ -108,12 +111,12 @@ class HyperoptTools(): | |||||||
|                 result_dict.update(space_params) |                 result_dict.update(space_params) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _params_pretty_print(params, space: str, header: str) -> None: |     def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: | ||||||
|         if space in params: |         if space in params or space in non_optimized: | ||||||
|             space_params = HyperoptTools._space_params(params, space, 5) |             space_params = HyperoptTools._space_params(params, space, 5) | ||||||
|             params_result = f"\n# {header}\n" |             result = f"\n# {header}\n" | ||||||
|             if space == 'stoploss': |             if space == 'stoploss': | ||||||
|                 params_result += f"stoploss = {space_params.get('stoploss')}" |                 result += f"stoploss = {space_params.get('stoploss')}" | ||||||
|             elif space == 'roi': |             elif space == 'roi': | ||||||
|                 # TODO: get rid of OrderedDict when support for python 3.6 will be |                 # TODO: get rid of OrderedDict when support for python 3.6 will be | ||||||
|                 # dropped (dicts keep the order as the language feature) |                 # 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() |                         (str(k), v) for k, v in space_params.items() | ||||||
|                     ), |                     ), | ||||||
|                     default=str, indent=4, number_mode=rapidjson.NM_NATIVE) |                     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': |             elif space == 'trailing': | ||||||
|  |  | ||||||
|                 for k, v in space_params.items(): |                 for k, v in space_params.items(): | ||||||
|                     params_result += f'{k} = {v}\n' |                     result += f'{k} = {v}\n' | ||||||
|  |  | ||||||
|             else: |             else: | ||||||
|                 params_result += f"{space}_params = {pformat(space_params, indent=4)}" |                 no_params = HyperoptTools._space_params(non_optimized, space, 5) | ||||||
|                 params_result = params_result.replace("}", "\n}").replace("{", "{\n ") |  | ||||||
|  |  | ||||||
|             params_result = params_result.replace("\n", "\n    ") |                 result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}" | ||||||
|             print(params_result) |  | ||||||
|  |             result = result.replace("\n", "\n    ") | ||||||
|  |             print(result) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _space_params(params, space: str, r: int = None) -> Dict: |     def _space_params(params, space: str, r: int = None) -> Dict: | ||||||
|         d = params[space] |         d = params.get(space) | ||||||
|         # Round floats to `r` digits after the decimal point if requested |         if d: | ||||||
|         return round_dict(d, r) if r else 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 |     @staticmethod | ||||||
|     def is_best_loss(results, current_best_loss: float) -> bool: |     def is_best_loss(results, current_best_loss: float) -> bool: | ||||||
|         return results['loss'] < current_best_loss |         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 |     @staticmethod | ||||||
|     def _format_explanation_string(results, total_epochs) -> str: |     def _format_explanation_string(results, total_epochs) -> str: | ||||||
|         return (("*" if results['is_initial_point'] else " ") + |         return (("*" if results['is_initial_point'] else " ") + | ||||||
| @@ -168,12 +206,27 @@ class HyperoptTools(): | |||||||
|         if 'results_metrics.winsdrawslosses' not in trials.columns: |         if 'results_metrics.winsdrawslosses' not in trials.columns: | ||||||
|             # Ensure compatibility with older versions of hyperopt results |             # Ensure compatibility with older versions of hyperopt results | ||||||
|             trials['results_metrics.winsdrawslosses'] = 'N/A' |             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', |         trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', | ||||||
|                           'Total profit', 'Profit', 'Avg duration', 'Objective', |                           'Total profit', 'Profit', 'Avg duration', 'Objective', | ||||||
|                           'is_initial_point', 'is_best'] |                           '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['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' | ||||||
|         trials.loc[trials['Total profit'] > 0, 'is_profit'] = True |         trials.loc[trials['Total profit'] > 0, 'is_profit'] = True | ||||||
|         trials['Trades'] = trials['Trades'].astype(str) |         trials['Trades'] = trials['Trades'].astype(str) | ||||||
|  |         perc_multi = 1 if legacy_mode else 100 | ||||||
|         trials['Epoch'] = trials['Epoch'].apply( |         trials['Epoch'] = trials['Epoch'].apply( | ||||||
|             lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) |             lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) | ||||||
|         ) |         ) | ||||||
|         trials['Avg profit'] = trials['Avg profit'].apply( |         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( |         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( |         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( |         trials['Profit'] = trials.apply( | ||||||
|             lambda x: '{:,.8f} {} {}'.format( |             lambda x: '{} {}'.format( | ||||||
|                 x['Total profit'], config['stake_currency'], |                 round_coin_value(x['Total profit'], stake_currency), | ||||||
|                 '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') |                 '({:,.2f}%)'.format(x['Profit'] * perc_multi).rjust(10, ' ') | ||||||
|             ).rjust(25+len(config['stake_currency'])) |             ).rjust(25+len(stake_currency)) | ||||||
|             if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), |             if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), | ||||||
|             axis=1 |             axis=1 | ||||||
|         ) |         ) | ||||||
|         trials = trials.drop(columns=['Total profit']) |         trials = trials.drop(columns=['Total profit']) | ||||||
| @@ -263,11 +318,21 @@ class HyperoptTools(): | |||||||
|         trials['Best'] = '' |         trials['Best'] = '' | ||||||
|         trials['Stake currency'] = config['stake_currency'] |         trials['Stake currency'] = config['stake_currency'] | ||||||
|  |  | ||||||
|         base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', |         if 'results_metrics.total_trades' in trials: | ||||||
|                         'results_metrics.avg_profit', 'results_metrics.median_profit', |             base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', | ||||||
|                         'results_metrics.total_profit', |                             'results_metrics.profit_mean', 'results_metrics.profit_median', | ||||||
|                         'Stake currency', 'results_metrics.profit', 'results_metrics.duration', |                             'results_metrics.profit_total', | ||||||
|                         'loss', 'is_initial_point', 'is_best'] |                             '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()] |         param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] | ||||||
|         trials = trials[base_metrics + param_metrics] |         trials = trials[base_metrics + param_metrics] | ||||||
|  |  | ||||||
| @@ -284,21 +349,23 @@ class HyperoptTools(): | |||||||
|         trials.loc[trials['Total profit'] > 0, 'is_profit'] = True |         trials.loc[trials['Total profit'] > 0, 'is_profit'] = True | ||||||
|         trials['Epoch'] = trials['Epoch'].astype(str) |         trials['Epoch'] = trials['Epoch'].astype(str) | ||||||
|         trials['Trades'] = trials['Trades'].astype(str) |         trials['Trades'] = trials['Trades'].astype(str) | ||||||
|  |         trials['Median profit'] = trials['Median profit'] * perc_multi | ||||||
|  |  | ||||||
|         trials['Total profit'] = trials['Total profit'].apply( |         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( |         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( |         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( |         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( |         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']) |         trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) | ||||||
|   | |||||||
| @@ -153,7 +153,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List | |||||||
|     return tabular_data |     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 |     Generate summary per strategy | ||||||
|     :param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies |     :param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies | ||||||
| @@ -194,7 +194,37 @@ def generate_edge_table(results: dict) -> str: | |||||||
|                     floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")  # type: ignore |                     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]: | def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: | ||||||
|  |     """ Generate daily statistics """ | ||||||
|     if len(results) == 0: |     if len(results) == 0: | ||||||
|         return { |         return { | ||||||
|             'backtest_best_day': 0, |             'backtest_best_day': 0, | ||||||
| @@ -204,8 +234,6 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: | |||||||
|             'winning_days': 0, |             'winning_days': 0, | ||||||
|             'draw_days': 0, |             'draw_days': 0, | ||||||
|             'losing_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_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() | ||||||
|     daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) |     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) |     draw_days = sum(daily_profit == 0) | ||||||
|     losing_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 { |     return { | ||||||
|         'backtest_best_day': best_rel, |         'backtest_best_day': best_rel, | ||||||
|         'backtest_worst_day': worst_rel, |         'backtest_worst_day': worst_rel, | ||||||
| @@ -228,13 +253,148 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: | |||||||
|         'winning_days': winning_days, |         'winning_days': winning_days, | ||||||
|         'draw_days': draw_days, |         'draw_days': draw_days, | ||||||
|         'losing_days': losing_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], | def generate_backtest_stats(btdata: Dict[str, DataFrame], | ||||||
|                             all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], |                             all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], | ||||||
|                             min_date: Arrow, max_date: Arrow |                             min_date: Arrow, max_date: Arrow | ||||||
| @@ -245,132 +405,17 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], | |||||||
|                      { Strategy: {'results: results, 'config: config}}. |                      { Strategy: {'results: results, 'config: config}}. | ||||||
|     :param min_date: Backtest start date |     :param min_date: Backtest start date | ||||||
|     :param max_date: Backtest end date |     :param max_date: Backtest end date | ||||||
|     :return: |     :return: Dictionary containing results per strategy and a stratgy summary. | ||||||
|     Dictionary containing results per strategy and a stratgy summary. |  | ||||||
|     """ |     """ | ||||||
|     result: Dict[str, Any] = {'strategy': {}} |     result: Dict[str, Any] = {'strategy': {}} | ||||||
|     market_change = calculate_market_change(btdata, 'close') |     market_change = calculate_market_change(btdata, 'close') | ||||||
|  |  | ||||||
|     for strategy, content in all_results.items(): |     for strategy, content in all_results.items(): | ||||||
|         results: Dict[str, DataFrame] = content['results'] |         strat_stats = generate_strategy_stats(btdata, strategy, content, | ||||||
|         if not isinstance(results, DataFrame): |                                               min_date, max_date, market_change=market_change) | ||||||
|             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, |  | ||||||
|         } |  | ||||||
|         result['strategy'][strategy] = strat_stats |         result['strategy'][strategy] = strat_stats | ||||||
|  |  | ||||||
|         try: |     strategy_results = generate_strategy_comparison(all_results=all_results) | ||||||
|             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) |  | ||||||
|  |  | ||||||
|     result['strategy_comparison'] = strategy_results |     result['strategy_comparison'] = strategy_results | ||||||
|  |  | ||||||
| @@ -522,37 +567,43 @@ def text_table_add_metrics(strat_results: Dict) -> str: | |||||||
|         return message |         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): | def show_backtest_results(config: Dict, backtest_stats: Dict): | ||||||
|     stake_currency = config['stake_currency'] |     stake_currency = config['stake_currency'] | ||||||
|  |  | ||||||
|     for strategy, results in backtest_stats['strategy'].items(): |     for strategy, results in backtest_stats['strategy'].items(): | ||||||
|  |         show_backtest_result(strategy, results, stake_currency) | ||||||
|         # 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() |  | ||||||
|  |  | ||||||
|     if len(backtest_stats['strategy']) > 1: |     if len(backtest_stats['strategy']) > 1: | ||||||
|         # Print Strategy summary table |         # Print Strategy summary table | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ This module defines a base class for auto-hyperoptable strategies. | |||||||
| import logging | import logging | ||||||
| from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||||
| from contextlib import suppress | 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 | from freqtrade.optimize.hyperopt_tools import HyperoptTools | ||||||
|  |  | ||||||
| @@ -29,6 +29,7 @@ class BaseParameter(ABC): | |||||||
|     default: Any |     default: Any | ||||||
|     value: Any |     value: Any | ||||||
|     in_space: bool = False |     in_space: bool = False | ||||||
|  |     name: str | ||||||
|  |  | ||||||
|     def __init__(self, *, default: Any, space: Optional[str] = None, |     def __init__(self, *, default: Any, space: Optional[str] = None, | ||||||
|                  optimize: bool = True, load: bool = True, **kwargs): |                  optimize: bool = True, load: bool = True, **kwargs): | ||||||
| @@ -250,6 +251,9 @@ class HyperStrategyMixin(object): | |||||||
|         Initialize hyperoptable strategy mixin. |         Initialize hyperoptable strategy mixin. | ||||||
|         """ |         """ | ||||||
|         self.config = config |         self.config = config | ||||||
|  |         self.ft_buy_params: List[BaseParameter] = [] | ||||||
|  |         self.ft_sell_params: List[BaseParameter] = [] | ||||||
|  |  | ||||||
|         self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) |         self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) | ||||||
|  |  | ||||||
|     def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: |     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): |         if category not in ('buy', 'sell', None): | ||||||
|             raise OperationalException('Category must be one of: "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): |         for attr_name in dir(self): | ||||||
|             if not attr_name.startswith('__'):  # Ignore internals, not strictly necessary. |             if not attr_name.startswith('__'):  # Ignore internals, not strictly necessary. | ||||||
|                 attr = getattr(self, attr_name) |                 attr = getattr(self, attr_name) | ||||||
|                 if issubclass(attr.__class__, BaseParameter): |                 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): |                             and attr.category is not None and attr.category != category): | ||||||
|                         raise OperationalException( |                         raise OperationalException( | ||||||
|                             f'Inconclusive parameter name {attr_name}, category: {attr.category}.') |                             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)): |                             (attr_name.startswith(category + '_') and attr.category is None)): | ||||||
|                         yield attr_name, attr |                         yield attr_name, attr | ||||||
|  |  | ||||||
| @@ -286,9 +301,16 @@ class HyperStrategyMixin(object): | |||||||
|         """ |         """ | ||||||
|         if not params: |         if not params: | ||||||
|             logger.info(f"No params for {space} found, using default values.") |             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) |             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 params and attr_name in params: | ||||||
|                 if attr.load: |                 if attr.load: | ||||||
|                     attr.value = params[attr_name] |                     attr.value = params[attr_name] | ||||||
| @@ -298,3 +320,16 @@ class HyperStrategyMixin(object): | |||||||
|                                    f'Default value "{attr.value}" used.') |                                    f'Default value "{attr.value}" used.') | ||||||
|             else: |             else: | ||||||
|                 logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') |                 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 | ||||||
|   | |||||||
| @@ -918,242 +918,244 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): | |||||||
|                     captured.out) |                     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( |     mocker.patch( | ||||||
|         'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', |         'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', | ||||||
|         MagicMock(return_value=hyperopt_results) |         MagicMock(return_value=saved_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) |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     args = [ |     args = [ | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import json | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| from copy import deepcopy | from copy import deepcopy | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from functools import reduce | from functools import reduce | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from unittest.mock import MagicMock, Mock, PropertyMock | from unittest.mock import MagicMock, Mock, PropertyMock | ||||||
| @@ -1778,7 +1778,7 @@ def open_trade(): | |||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| def hyperopt_results(): | def saved_hyperopt_results_legacy(): | ||||||
|     return [ |     return [ | ||||||
|         { |         { | ||||||
|             'loss': 0.4366182531161519, |             'loss': 0.4366182531161519, | ||||||
| @@ -1907,3 +1907,136 @@ def hyperopt_results(): | |||||||
|             'is_best': False |             '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 | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|   | |||||||
| @@ -501,13 +501,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: | |||||||
|     # Dummy data as we mock the analyze functions |     # Dummy data as we mock the analyze functions | ||||||
|     data_processed = {pair: frame.copy()} |     data_processed = {pair: frame.copy()} | ||||||
|     min_date, max_date = get_timerange({pair: frame}) |     min_date, max_date = get_timerange({pair: frame}) | ||||||
|     results = backtesting.backtest( |     result = backtesting.backtest( | ||||||
|         processed=data_processed, |         processed=data_processed, | ||||||
|         start_date=min_date, |         start_date=min_date, | ||||||
|         end_date=max_date, |         end_date=max_date, | ||||||
|         max_open_trades=10, |         max_open_trades=10, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     results = result['results'] | ||||||
|     assert len(results) == len(data.trades) |     assert len(results) == len(data.trades) | ||||||
|     assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) |     assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -514,13 +514,14 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: | |||||||
|                              timerange=timerange) |                              timerange=timerange) | ||||||
|     processed = backtesting.strategy.ohlcvdata_to_dataframe(data) |     processed = backtesting.strategy.ohlcvdata_to_dataframe(data) | ||||||
|     min_date, max_date = get_timerange(processed) |     min_date, max_date = get_timerange(processed) | ||||||
|     results = backtesting.backtest( |     result = backtesting.backtest( | ||||||
|         processed=processed, |         processed=processed, | ||||||
|         start_date=min_date, |         start_date=min_date, | ||||||
|         end_date=max_date, |         end_date=max_date, | ||||||
|         max_open_trades=10, |         max_open_trades=10, | ||||||
|         position_stacking=False, |         position_stacking=False, | ||||||
|     ) |     ) | ||||||
|  |     results = result['results'] | ||||||
|     assert not results.empty |     assert not results.empty | ||||||
|     assert len(results) == 2 |     assert len(results) == 2 | ||||||
|  |  | ||||||
| @@ -583,8 +584,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None | |||||||
|         max_open_trades=1, |         max_open_trades=1, | ||||||
|         position_stacking=False, |         position_stacking=False, | ||||||
|     ) |     ) | ||||||
|     assert not results.empty |     assert not results['results'].empty | ||||||
|     assert len(results) == 1 |     assert len(results['results']) == 1 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_processed(default_conf, mocker, testdatadir) -> None: | 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 |     # While buy-signals are unrealistic, running backtesting | ||||||
|     # over and over again should not cause different results |     # over and over again should not cause different results | ||||||
|     for [contour, numres] in tests: |     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', [ | @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) |     mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) | ||||||
|     # While buy-signals are unrealistic, running backtesting |     # While buy-signals are unrealistic, running backtesting | ||||||
|     # over and over again should not cause different results |     # 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): | 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 = Backtesting(default_conf) | ||||||
|     backtesting.strategy.advise_buy = fun  # Override |     backtesting.strategy.advise_buy = fun  # Override | ||||||
|     backtesting.strategy.advise_sell = fun  # Override |     backtesting.strategy.advise_sell = fun  # Override | ||||||
|     results = backtesting.backtest(**backtest_conf) |     result = backtesting.backtest(**backtest_conf) | ||||||
|     assert results.empty |     assert result['results'].empty | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_backtest_only_sell(mocker, default_conf, testdatadir): | 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 = Backtesting(default_conf) | ||||||
|     backtesting.strategy.advise_buy = fun  # Override |     backtesting.strategy.advise_buy = fun  # Override | ||||||
|     backtesting.strategy.advise_sell = fun  # Override |     backtesting.strategy.advise_sell = fun  # Override | ||||||
|     results = backtesting.backtest(**backtest_conf) |     result = backtesting.backtest(**backtest_conf) | ||||||
|     assert results.empty |     assert result['results'].empty | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): | 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 = Backtesting(default_conf) | ||||||
|     backtesting.strategy.advise_buy = _trend_alternate  # Override |     backtesting.strategy.advise_buy = _trend_alternate  # Override | ||||||
|     backtesting.strategy.advise_sell = _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 |     # 200 candles in backtest data | ||||||
|     # won't buy on first (shifted by 1) |     # won't buy on first (shifted by 1) | ||||||
|     # 100 buys signals |     # 100 buys signals | ||||||
|  |     results = result['results'] | ||||||
|     assert len(results) == 100 |     assert len(results) == 100 | ||||||
|     # One trade was force-closed at the end |     # One trade was force-closed at the end | ||||||
|     assert len(results.loc[results['is_open']]) == 0 |     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) |     results = backtesting.backtest(**backtest_conf) | ||||||
|  |  | ||||||
|     # Make sure we have parallel trades |     # 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 |     # 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 = { |     backtest_conf = { | ||||||
|         'processed': processed, |         'processed': processed, | ||||||
| @@ -757,7 +759,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) | |||||||
|         'position_stacking': False, |         'position_stacking': False, | ||||||
|     } |     } | ||||||
|     results = backtesting.backtest(**backtest_conf) |     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): | 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") | @pytest.mark.filterwarnings("ignore:deprecated") | ||||||
| def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): | 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) |     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', |     mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', | ||||||
|                  PropertyMock(return_value=['UNITTEST/BTC'])) |                  PropertyMock(return_value=['UNITTEST/BTC'])) | ||||||
|     mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) |     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, |                           text_table_strategy=strattable_mock, | ||||||
|                           generate_pair_metrics=MagicMock(), |                           generate_pair_metrics=MagicMock(), | ||||||
|                           generate_sell_reason_stats=sell_reason_mock, |                           generate_sell_reason_stats=sell_reason_mock, | ||||||
|                           generate_strategy_metrics=strat_summary, |                           generate_strategy_comparison=strat_summary, | ||||||
|                           generate_daily_stats=MagicMock(), |                           generate_daily_stats=MagicMock(), | ||||||
|                           ) |                           ) | ||||||
|     patched_configuration_load_config_file(mocker, default_conf) |     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") | @pytest.mark.filterwarnings("ignore:deprecated") | ||||||
| def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): | 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) |     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=[ |     backtestmock = MagicMock(side_effect=[ | ||||||
|         pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], |         { | ||||||
|                       'profit_ratio': [0.0, 0.0], |             'results': result1, | ||||||
|                       'profit_abs': [0.0, 0.0], |             'config': default_conf, | ||||||
|                       'open_date': pd.to_datetime(['2018-01-29 18:40:00', |             'locks': [], | ||||||
|                                                    '2018-01-30 03:30:00', ], utc=True |             'final_balance': 1000, | ||||||
|                                                   ), |         }, | ||||||
|                       'close_date': pd.to_datetime(['2018-01-29 20:45:00', |         { | ||||||
|                                                     '2018-01-30 05:35:00', ], utc=True), |             'results': result2, | ||||||
|                       'trade_duration': [235, 40], |             'config': default_conf, | ||||||
|                       'is_open': [False, False], |             'locks': [], | ||||||
|                       'stake_amount': [0.01, 0.01], |             'final_balance': 1000, | ||||||
|                       '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] |  | ||||||
|                       }), |  | ||||||
|     ]) |     ]) | ||||||
|     mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', |     mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', | ||||||
|                  PropertyMock(return_value=['UNITTEST/BTC'])) |                  PropertyMock(return_value=['UNITTEST/BTC'])) | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import re | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Dict, List | from typing import Dict, List | ||||||
| from unittest.mock import MagicMock | from unittest.mock import ANY, MagicMock | ||||||
|  |  | ||||||
| import pandas as pd | import pandas as pd | ||||||
| import pytest | import pytest | ||||||
| @@ -18,10 +18,12 @@ from freqtrade.exceptions import OperationalException | |||||||
| from freqtrade.optimize.hyperopt import Hyperopt | from freqtrade.optimize.hyperopt import Hyperopt | ||||||
| from freqtrade.optimize.hyperopt_auto import HyperOptAuto | from freqtrade.optimize.hyperopt_auto import HyperOptAuto | ||||||
| from freqtrade.optimize.hyperopt_tools import HyperoptTools | from freqtrade.optimize.hyperopt_tools import HyperoptTools | ||||||
|  | from freqtrade.optimize.optimize_reports import generate_strategy_stats | ||||||
| from freqtrade.optimize.space import SKDecimal | from freqtrade.optimize.space import SKDecimal | ||||||
| from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver | from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver | ||||||
| from freqtrade.state import RunMode | from freqtrade.state import RunMode | ||||||
| from freqtrade.strategy.hyper import IntParameter | 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, | from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, | ||||||
|                             patched_configuration_load_config_file) |                             patched_configuration_load_config_file) | ||||||
|  |  | ||||||
| @@ -433,18 +435,41 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: | |||||||
|     assert hasattr(hyperopt, "position_stacking") |     assert hasattr(hyperopt, "position_stacking") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_format_results(hyperopt): | def test_hyperopt_format_results(hyperopt): | ||||||
|     # Test with BTC as stake_currency |  | ||||||
|     trades = [ |     bt_result = { | ||||||
|         ('ETH/BTC', 2, 2, 123), |         'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", | ||||||
|         ('LTC/BTC', 1, 1, 123), |                                           "UNITTEST/BTC", "UNITTEST/BTC"], | ||||||
|         ('XPR/BTC', -1, -2, -246) |                                  "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], | ||||||
|     ] |                                  "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], | ||||||
|     labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] |                                  "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, | ||||||
|     df = pd.DataFrame.from_records(trades, columns=labels) |                                                Arrow(2017, 11, 14, 21, 36, 00).datetime, | ||||||
|     results_metrics = hyperopt._calculate_results_metrics(df) |                                                Arrow(2017, 11, 14, 22, 12, 00).datetime, | ||||||
|     results_explanation = hyperopt._format_results_explanation_string(results_metrics) |                                                Arrow(2017, 11, 14, 22, 44, 00).datetime], | ||||||
|     total_profit = results_metrics['total_profit'] |                                  "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 = { |     results = { | ||||||
|         'loss': 0.0, |         'loss': 0.0, | ||||||
| @@ -458,21 +483,9 @@ def test_format_results(hyperopt): | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     result = HyperoptTools._format_explanation_string(results, 1) |     result = HyperoptTools._format_explanation_string(results, 1) | ||||||
|     assert result.find(' 66.67%') |     assert ' 0.71%' in result | ||||||
|     assert result.find('Total profit 1.00000000 BTC') |     assert 'Total profit  0.00003100 BTC' in result | ||||||
|     assert result.find('2.0000Σ %') |     assert '0:50:00 min' in result | ||||||
|  |  | ||||||
|     # 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') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize("spaces, expected_results", [ | @pytest.mark.parametrize("spaces, expected_results", [ | ||||||
| @@ -577,22 +590,37 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: | |||||||
|                           'hyperopt_min_trades': 1, |                           'hyperopt_min_trades': 1, | ||||||
|                           }) |                           }) | ||||||
|  |  | ||||||
|     trades = [ |     backtest_result = { | ||||||
|         ('TRX/BTC', 0.023117, 0.000233, 100) |         'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", | ||||||
|     ] |                                           "UNITTEST/BTC", "UNITTEST/BTC"], | ||||||
|     labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration'] |                                  "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], | ||||||
|     backtest_result = pd.DataFrame.from_records(trades, columns=labels) |                                  "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( |     mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) | ||||||
|         'freqtrade.optimize.hyperopt.Backtesting.backtest', |     mocker.patch('freqtrade.optimize.hyperopt.get_timerange', | ||||||
|         MagicMock(return_value=backtest_result) |                  return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) | ||||||
|     ) |  | ||||||
|     mocker.patch( |  | ||||||
|         'freqtrade.optimize.hyperopt.get_timerange', |  | ||||||
|         MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) |  | ||||||
|     ) |  | ||||||
|     patch_exchange(mocker) |     patch_exchange(mocker) | ||||||
|     mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) |     mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) | ||||||
|  |  | ||||||
|     optimizer_param = { |     optimizer_param = { | ||||||
|         'adx-value': 0, |         'adx-value': 0, | ||||||
| @@ -626,11 +654,11 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: | |||||||
|         'trailing_only_offset_is_reached': False, |         'trailing_only_offset_is_reached': False, | ||||||
|     } |     } | ||||||
|     response_expected = { |     response_expected = { | ||||||
|         'loss': 1.9840569076926293, |         'loss': 1.9147239021396234, | ||||||
|         'results_explanation': ('     1 trades. 1/0/0 Wins/Draws/Losses. ' |         'results_explanation': ('     4 trades. 4/0/0 Wins/Draws/Losses. ' | ||||||
|                                 'Avg profit   2.31%. Median profit   2.31%. Total profit  ' |                                 'Avg profit   0.77%. Median profit   0.71%. Total profit  ' | ||||||
|                                 '0.00023300 BTC (   2.31\N{GREEK CAPITAL LETTER SIGMA}%). ' |                                 '0.00003100 BTC (   0.00\N{GREEK CAPITAL LETTER SIGMA}%). ' | ||||||
|                                 'Avg duration 100.0 min.' |                                 'Avg duration 0:50:00 min.' | ||||||
|                                 ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), |                                 ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), | ||||||
|         'params_details': {'buy': {'adx-enabled': False, |         'params_details': {'buy': {'adx-enabled': False, | ||||||
|                                    'adx-value': 0, |                                    'adx-value': 0, | ||||||
| @@ -660,21 +688,16 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: | |||||||
|                                         'trailing_stop_positive': 0.02, |                                         'trailing_stop_positive': 0.02, | ||||||
|                                         'trailing_stop_positive_offset': 0.07}}, |                                         'trailing_stop_positive_offset': 0.07}}, | ||||||
|         'params_dict': optimizer_param, |         'params_dict': optimizer_param, | ||||||
|         'results_metrics': {'avg_profit': 2.3117, |         'params_not_optimized': {'buy': {}, 'sell': {}}, | ||||||
|                             'draws': 0, |         'results_metrics': ANY, | ||||||
|                             'duration': 100.0, |         'total_profit': 3.1e-08 | ||||||
|                             '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 |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     hyperopt = Hyperopt(hyperopt_conf) |     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())) |     generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) | ||||||
|     assert generate_optimizer_value == response_expected |     assert generate_optimizer_value == response_expected | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,8 @@ from freqtrade.edge import PairInfo | |||||||
| from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, | from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, | ||||||
|                                                  generate_edge_table, generate_pair_metrics, |                                                  generate_edge_table, generate_pair_metrics, | ||||||
|                                                  generate_sell_reason_stats, |                                                  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_bt_results, text_table_sell_reason, | ||||||
|                                                  text_table_strategy) |                                                  text_table_strategy) | ||||||
| from freqtrade.resolvers.strategy_resolver import StrategyResolver | from freqtrade.resolvers.strategy_resolver import StrategyResolver | ||||||
| @@ -226,8 +227,6 @@ def test_generate_daily_stats(testdatadir): | |||||||
|     assert res['winning_days'] == 14 |     assert res['winning_days'] == 14 | ||||||
|     assert res['draw_days'] == 4 |     assert res['draw_days'] == 4 | ||||||
|     assert res['losing_days'] == 3 |     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! |     # Select empty dataframe! | ||||||
|     res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) |     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 |     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(): | def test_text_table_sell_reason(): | ||||||
|  |  | ||||||
|     results = pd.DataFrame( |     results = pd.DataFrame( | ||||||
| @@ -345,7 +361,7 @@ def test_text_table_strategy(default_conf): | |||||||
|         '          43.33 |        0:20:00 |      3 |       0 |        0 |' |         '          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 |     assert text_table_strategy(strategy_results, 'BTC') == result_str | ||||||
|  |  | ||||||
|   | |||||||
| @@ -671,4 +671,4 @@ def test_auto_hyperopt_interface(default_conf): | |||||||
|     strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') |     strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') | ||||||
|  |  | ||||||
|     with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): |     with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): | ||||||
|         [x for x in strategy.enumerate_parameters('sell')] |         [x for x in strategy._detect_parameters('sell')] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user