diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 29d0d98a2..fa1d77407 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -49,8 +49,14 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "ticker_interval"] +ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "print_colorized", + "print_json", "hyperopt_list_no_details"] + +ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", + "print_json", "hyperopt_show_no_header"] + NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", - "plot-dataframe", "plot-profit"] + "hyperopt_list", "hyperopt_show", "plot-dataframe", "plot-profit"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] @@ -116,6 +122,7 @@ class Arguments: from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import (start_create_userdir, start_download_data, + start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_markets, start_list_timeframes, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit @@ -220,3 +227,21 @@ class Arguments: ) plot_profit_cmd.set_defaults(func=start_plot_profit) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) + + # Add hyperopt-list subcommand + hyperopt_list_cmd = subparsers.add_parser( + 'hyperopt-list', + help='List Hyperopt results', + parents=[_common_parser], + ) + hyperopt_list_cmd.set_defaults(func=start_hyperopt_list) + self._build_args(optionlist=ARGS_HYPEROPT_LIST, parser=hyperopt_list_cmd) + + # Add hyperopt-show subcommand + hyperopt_show_cmd = subparsers.add_parser( + 'hyperopt-show', + help='Show details of Hyperopt results', + parents=[_common_parser], + ) + hyperopt_show_cmd.set_defaults(func=start_hyperopt_show) + self._build_args(optionlist=ARGS_HYPEROPT_SHOW, parser=hyperopt_show_cmd) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 6dc5ef026..beed2b835 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -18,6 +18,18 @@ def check_int_positive(value: str) -> int: return uint +def check_int_nonzero(value: str) -> int: + try: + uint = int(value) + if uint == 0: + raise ValueError + except ValueError: + raise argparse.ArgumentTypeError( + f"{value} is invalid for this parameter, should be a non-zero integer value" + ) + return uint + + class Arg: # Optional CLI arguments def __init__(self, *args, **kwargs): @@ -364,4 +376,31 @@ AVAILABLE_CLI_OPTIONS = { choices=["DB", "file"], default="file", ), + # hyperopt-list, hyperopt-show + "hyperopt_list_profitable": Arg( + '--profitable', + help='Select only profitable epochs.', + action='store_true', + ), + "hyperopt_list_best": Arg( + '--best', + help='Select only best epochs.', + action='store_true', + ), + "hyperopt_list_no_details": Arg( + '--no-details', + help='Do not print best epoch details.', + action='store_true', + ), + "hyperopt_show_index": Arg( + '-n', '--index', + help='Specify the index of the epoch to print details for.', + type=check_int_nonzero, + metavar='INT', + ), + "hyperopt_show_no_header": Arg( + '--no-header', + help='Do not print epoch details header.', + action='store_true', + ), } diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 93eee3912..6552078c8 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -308,6 +308,21 @@ class Configuration: self._args_to_config(config, argname='hyperopt_loss', logstring='Using Hyperopt loss class name: {}') + self._args_to_config(config, argname='hyperopt_show_index', + logstring='Parameter -n/--index detected: {}') + + self._args_to_config(config, argname='hyperopt_list_best', + logstring='Parameter --best detected: {}') + + self._args_to_config(config, argname='hyperopt_list_profitable', + logstring='Parameter --profitable detected: {}') + + self._args_to_config(config, argname='hyperopt_list_no_details', + logstring='Parameter --no-details detected: {}') + + self._args_to_config(config, argname='hyperopt_show_no_header', + logstring='Parameter --no-header detected: {}') + def _process_plot_options(self, config: Dict[str, Any]) -> None: self._args_to_config(config, argname='pairs', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 836309a62..2ef8ddc2f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -74,11 +74,11 @@ class Hyperopt: else: logger.info("Continuing on previous hyperopt results.") + self.num_trials_saved = 0 + # Previous evaluations self.trials: List = [] - self.num_trials_saved = 0 - # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_indicators'): self.backtesting.strategy.advise_indicators = \ @@ -104,6 +104,10 @@ class Hyperopt: self.config['ask_strategy'] = {} self.config['ask_strategy']['use_sell_signal'] = True + self.print_all = self.config.get('print_all', False) + self.print_colorized = self.config.get('print_colorized', False) + self.print_json = self.config.get('print_json', False) + @staticmethod def get_lock_filename(config) -> str: @@ -119,20 +123,18 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() - def get_args(self, params): + def _get_params_dict(self, raw_params: List[Any]) -> Dict: - dimensions = self.dimensions + dimensions: List[Dimension] = self.dimensions # Ensure the number of dimensions match - # the number of parameters in the list x. - if len(params) != len(dimensions): - raise ValueError('Mismatch in number of search-space dimensions. ' - f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}') + # the number of parameters in the list. + if len(raw_params) != len(dimensions): + raise ValueError('Mismatch in number of search-space dimensions.') - # Create a dict where the keys are the names of the dimensions - # and the values are taken from the list of parameters x. - arg_dict = {dim.name: value for dim, value in zip(dimensions, params)} - return arg_dict + # Return a dict where the keys are the names of the dimensions + # and the values are taken from the list of parameters. + return {d.name: v for d, v in zip(dimensions, raw_params)} def save_trials(self, final: bool = False) -> None: """ @@ -147,106 +149,126 @@ class Hyperopt: logger.info(f"{num_trials} {plural(num_trials, 'epoch')} " f"saved to '{self.trials_file}'.") - def read_trials(self) -> List: + @staticmethod + def _read_trials(trials_file) -> List: """ Read hyperopt trials file """ - logger.info("Reading Trials from '%s'", self.trials_file) - trials = load(self.trials_file) - self.trials_file.unlink() + logger.info("Reading Trials from '%s'", trials_file) + trials = load(trials_file) return trials - def log_trials_result(self) -> None: + def _get_params_details(self, params: Dict) -> Dict: """ - Display Best hyperopt result + Return the params for each space """ - # This is printed when Ctrl+C is pressed quickly, before first epochs have - # a chance to be evaluated. - if not self.trials: - print("No epochs evaluated yet, no best result.") - return + result: Dict = {} - results = sorted(self.trials, key=itemgetter('loss')) - best_result = results[0] - params = best_result['params'] - log_str = self.format_results_logstring(best_result) - print(f"\nBest result:\n\n{log_str}\n") + if self.has_space('buy'): + result['buy'] = {p.name: params.get(p.name) for p in self.hyperopt_space('buy')} + if self.has_space('sell'): + result['sell'] = {p.name: params.get(p.name) for p in self.hyperopt_space('sell')} + if self.has_space('roi'): + result['roi'] = self.custom_hyperopt.generate_roi_table(params) + if self.has_space('stoploss'): + result['stoploss'] = params.get('stoploss') - if self.config.get('print_json'): + return result + + @staticmethod # noqa: C901 + def print_epoch_details(results, total_epochs, print_json: bool, + no_header: bool = False, header_str: str = None) -> None: + """ + Display details of the hyperopt result + """ + params = results['params_details'] + + # Default header string + if header_str is None: + header_str = "Best result" + + if not no_header: + explanation_str = Hyperopt._format_explanation_string(results, total_epochs) + print(f"\n{header_str}:\n\n{explanation_str}\n") + + if print_json: result_dict: Dict = {} - if self.has_space('buy') or self.has_space('sell'): - result_dict['params'] = {} - if self.has_space('buy'): - result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('buy')}) - if self.has_space('sell'): - result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('sell')}) - if self.has_space('roi'): + result_params_dict: Dict = {} + if 'buy' in params: + result_params_dict.update(params['buy']) + if 'sell' in params: + result_params_dict.update(params['sell']) + if result_params_dict: + result_dict['params'] = result_params_dict + if 'roi' in params: # Convert keys in min_roi dict to strings because # rapidjson cannot dump dicts with integer keys... # OrderedDict is used to keep the numeric order of the items # in the dict. result_dict['minimal_roi'] = OrderedDict( - (str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items() + (str(k), v) for k, v in params['roi'].items() ) - if self.has_space('stoploss'): - result_dict['stoploss'] = params.get('stoploss') + if 'stoploss' in params: + result_dict['stoploss'] = params['stoploss'] print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: - if self.has_space('buy'): + if 'buy' in params: print('Buy hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, - indent=4) - if self.has_space('sell'): + pprint(params['buy'], indent=4) + if 'sell' in params: print('Sell hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, - indent=4) - if self.has_space('roi'): + pprint(params['sell'], indent=4) + if 'roi' in params: print("ROI table:") # Round printed values to 5 digits after the decimal point - pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4) - if self.has_space('stoploss'): + pprint(round_dict(params['roi'], 5), indent=4) + if 'stoploss' in params: # Also round to 5 digits after the decimal point - print(f"Stoploss: {round(params.get('stoploss'), 5)}") + print(f"Stoploss: {round(params['stoploss'], 5)}") - def is_best(self, results) -> bool: - return results['loss'] < self.current_best_loss + @staticmethod + def is_best_loss(results, current_best_loss) -> bool: + return results['loss'] < current_best_loss - def log_results(self, results) -> None: + def print_results(self, results) -> None: """ Log results if it is better than any previous evaluation """ - print_all = self.config.get('print_all', False) - is_best_loss = self.is_best(results) - - if not print_all: + is_best = results['is_best'] + if not self.print_all: + # Print '\n' after each 100th epoch to separate dots from the log messages. + # Otherwise output is messy on a terminal. print('.', end='' if results['current_epoch'] % 100 != 0 else None) # type: ignore sys.stdout.flush() - if print_all or is_best_loss: - if is_best_loss: - self.current_best_loss = results['loss'] - log_str = self.format_results_logstring(results) - # Colorize output - if self.config.get('print_colorized', False): - if results['total_profit'] > 0: - log_str = Fore.GREEN + log_str - if print_all and is_best_loss: - log_str = Style.BRIGHT + log_str - if print_all: - print(log_str) - else: - print(f'\n{log_str}') + if self.print_all or is_best: + if not self.print_all: + # Separate the results explanation string from dots + print("\n") + self.print_results_explanation(results, self.total_epochs, self.print_all, + self.print_colorized) - def format_results_logstring(self, results) -> str: - current = results['current_epoch'] - total = self.total_epochs - res = results['results_explanation'] - loss = results['loss'] - log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' - log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}' - return log_str + @staticmethod + def print_results_explanation(results, total_epochs, highlight_best: bool, + print_colorized: bool) -> None: + """ + Log results explanation string + """ + explanation_str = Hyperopt._format_explanation_string(results, total_epochs) + # Colorize output + if print_colorized: + if results['total_profit'] > 0: + explanation_str = Fore.GREEN + explanation_str + if highlight_best and results['is_best']: + explanation_str = Style.BRIGHT + explanation_str + print(explanation_str) + + @staticmethod + def _format_explanation_string(results, total_epochs) -> str: + return (("*" if results['is_initial_point'] else " ") + + f"{results['current_epoch']:5d}/{total_epochs}: " + + f"{results['results_explanation']} " + + f"Objective: {results['loss']:.5f}") def has_space(self, space: str) -> bool: """ @@ -276,33 +298,34 @@ class Hyperopt: spaces += self.custom_hyperopt.stoploss_space() return spaces - def generate_optimizer(self, _params: Dict, 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. Keep this function as optimized as possible! """ - params = self.get_args(_params) + params_dict = self._get_params_dict(raw_params) + params_details = self._get_params_details(params_dict) if self.has_space('roi'): self.backtesting.strategy.minimal_roi = \ - self.custom_hyperopt.generate_roi_table(params) + self.custom_hyperopt.generate_roi_table(params_dict) if self.has_space('buy'): self.backtesting.strategy.advise_buy = \ - self.custom_hyperopt.buy_strategy_generator(params) + self.custom_hyperopt.buy_strategy_generator(params_dict) if self.has_space('sell'): self.backtesting.strategy.advise_sell = \ - self.custom_hyperopt.sell_strategy_generator(params) + self.custom_hyperopt.sell_strategy_generator(params_dict) if self.has_space('stoploss'): - self.backtesting.strategy.stoploss = params['stoploss'] + self.backtesting.strategy.stoploss = params_dict['stoploss'] processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) - results = self.backtesting.backtest( + backtesting_results = self.backtesting.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, @@ -312,58 +335,58 @@ class Hyperopt: 'end_date': max_date, } ) - results_explanation = self.format_results(results) + results_metrics = self._calculate_results_metrics(backtesting_results) + results_explanation = self._format_results_explanation_string(results_metrics) - trade_count = len(results.index) - total_profit = results.profit_abs.sum() + trade_count = results_metrics['trade_count'] + total_profit = results_metrics['total_profit'] # If this evaluation contains too short amount of trades to be # interesting -- consider it as 'bad' (assigned max. loss value) # in order to cast this hyperspace point away from optimization # path. We do not want to optimize 'hodl' strategies. - if trade_count < self.config['hyperopt_min_trades']: - return { - 'loss': MAX_LOSS, - 'params': params, - 'results_explanation': results_explanation, - 'total_profit': total_profit, - } - - loss = self.calculate_loss(results=results, trade_count=trade_count, - min_date=min_date.datetime, max_date=max_date.datetime) - + loss: float = MAX_LOSS + if trade_count >= self.config['hyperopt_min_trades']: + loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, + min_date=min_date.datetime, max_date=max_date.datetime) return { 'loss': loss, - 'params': params, + 'params_dict': params_dict, + 'params_details': params_details, + 'results_metrics': results_metrics, 'results_explanation': results_explanation, 'total_profit': total_profit, } - def format_results(self, results: DataFrame) -> str: + def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: + return { + 'trade_count': len(backtesting_results.index), + 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, + 'total_profit': backtesting_results.profit_abs.sum(), + 'profit': backtesting_results.profit_percent.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 """ - trades = len(results.index) - avg_profit = results.profit_percent.mean() * 100.0 - total_profit = results.profit_abs.sum() stake_cur = self.config['stake_currency'] - profit = results.profit_percent.sum() * 100.0 - duration = results.trade_duration.mean() - - return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. ' - f'Total profit {total_profit: 11.8f} {stake_cur} ' - f'({profit: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). ' - f'Avg duration {duration:5.1f} mins.' + return (f"{results_metrics['trade_count']:6d} trades. " + f"Avg profit {results_metrics['avg_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} mins." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') - def get_optimizer(self, dimensions, cpu_count) -> Optimizer: + def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: return Optimizer( dimensions, base_estimator="ET", acq_optimizer="auto", n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, - random_state=self.config.get('hyperopt_random_state', None) + random_state=self.config.get('hyperopt_random_state', None), ) def fix_optimizer_models_list(self): @@ -387,14 +410,16 @@ class Hyperopt: return parallel(delayed( wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) - def load_previous_results(self): - """ read trials file if we have one """ - if self.trials_file.is_file() and self.trials_file.stat().st_size > 0: - self.trials = self.read_trials() - logger.info( - 'Loaded %d previous evaluations from disk.', - len(self.trials) - ) + @staticmethod + def load_previous_results(trials_file) -> List: + """ + Load data for epochs from the file if we have one + """ + trials: List = [] + if trials_file.is_file() and trials_file.stat().st_size > 0: + trials = Hyperopt._read_trials(trials_file) + logger.info(f"Loaded {len(trials)} previous evaluations from disk.") + return trials def start(self) -> None: data, timerange = self.backtesting.load_bt_data() @@ -415,17 +440,17 @@ class Hyperopt: # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange = None # type: ignore - self.load_previous_results() + self.trials = self.load_previous_results(self.trials_file) cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") config_jobs = self.config.get('hyperopt_jobs', -1) logger.info(f'Number of parallel jobs set as: {config_jobs}') - self.dimensions = self.hyperopt_space() + self.dimensions: List[Dimension] = self.hyperopt_space() self.opt = self.get_optimizer(self.dimensions, config_jobs) - if self.config.get('print_colorized', False): + if self.print_colorized: colorama_init(autoreset=True) try: @@ -439,19 +464,38 @@ class Hyperopt: self.opt.tell(asked, [v['loss'] for v in f_val]) self.fix_optimizer_models_list() for j in range(jobs): - # Use human-friendly index here (starting from 1) + # Use human-friendly indexes here (starting from 1) current = i * jobs + j + 1 val = f_val[j] val['current_epoch'] = current val['is_initial_point'] = current <= INITIAL_POINTS logger.debug(f"Optimizer epoch evaluated: {val}") - is_best = self.is_best(val) - self.log_results(val) + + is_best = self.is_best_loss(val, self.current_best_loss) + # This value is assigned here and not in the optimization method + # to keep proper order in the list of results. That's because + # evaluations can take different time. Here they are aligned in the + # order they will be shown to the user. + val['is_best'] = is_best + + self.print_results(val) + + if is_best: + self.current_best_loss = val['loss'] self.trials.append(val) + # Save results after each best epoch and every 100 epochs if is_best or current % 100 == 0: self.save_trials() except KeyboardInterrupt: print('User interrupted..') self.save_trials(final=True) - self.log_trials_result() + + if self.trials: + sorted_trials = sorted(self.trials, key=itemgetter('loss')) + results = sorted_trials[0] + self.print_epoch_details(results, self.total_epochs, self.print_json) + else: + # This is printed when Ctrl+C is pressed quickly, before first epochs have + # a chance to be evaluated. + print("No epochs evaluated yet, no best result.") diff --git a/freqtrade/utils.py b/freqtrade/utils.py index b8ab7504e..0139006d6 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,12 +1,14 @@ import logging import sys from collections import OrderedDict +from operator import itemgetter from pathlib import Path from typing import Any, Dict, List import arrow import csv import rapidjson +from colorama import init as colorama_init from tabulate import tabulate from freqtrade import OperationalException @@ -18,6 +20,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, symbol_is_pair) from freqtrade.misc import plural +from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -236,3 +239,99 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: args.get('list_pairs_print_json', False) or args.get('print_csv', False)): print(f"{summary_str}.") + + +def start_hyperopt_list(args: Dict[str, Any]) -> None: + """ + """ + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + only_best = config.get('hyperopt_list_best', False) + only_profitable = config.get('hyperopt_list_profitable', False) + print_colorized = config.get('print_colorized', False) + print_json = config.get('print_json', False) + no_details = config.get('hyperopt_list_no_details', False) + no_header = False + + trials_file = (config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_results.pickle') + + # Previous evaluations + trials = Hyperopt.load_previous_results(trials_file) + total_epochs = len(trials) + + trials = _hyperopt_filter_trials(trials, only_best, only_profitable) + + # TODO: fetch the interval for epochs to print from the cli option + epoch_start, epoch_stop = 0, None + + if print_colorized: + colorama_init(autoreset=True) + + try: + # Human-friendly indexes used here (starting from 1) + for val in trials[epoch_start:epoch_stop]: + Hyperopt.print_results_explanation(val, total_epochs, not only_best, print_colorized) + + except KeyboardInterrupt: + print('User interrupted..') + + if trials and not no_details: + sorted_trials = sorted(trials, key=itemgetter('loss')) + results = sorted_trials[0] + Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) + + +def start_hyperopt_show(args: Dict[str, Any]) -> None: + """ + """ + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + only_best = config.get('hyperopt_list_best', False) + only_profitable = config.get('hyperopt_list_profitable', False) + no_header = config.get('hyperopt_show_no_header', False) + + trials_file = (config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_results.pickle') + + # Previous evaluations + trials = Hyperopt.load_previous_results(trials_file) + total_epochs = len(trials) + + trials = _hyperopt_filter_trials(trials, only_best, only_profitable) + + n = config.get('hyperopt_show_index', -1) + if n > total_epochs: + raise OperationalException( + f"The index of the epoch to show should be less than {total_epochs + 1}.") + if n < -total_epochs: + raise OperationalException( + f"The index of the epoch to showshould be greater than {-total_epochs - 1}.") + + # Translate epoch index from human-readable format to pythonic + if n > 0: + n -= 1 + + print_json = config.get('print_json', False) + + if trials: + val = trials[n] + Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, + header_str="Epoch details") + + +def _hyperopt_filter_trials(trials: List, only_best: bool, only_profitable: bool) -> List: + """ + Filter our items from the list of hyperopt results + """ + if only_best: + trials = [x for x in trials if x['is_best']] + if only_profitable: + trials = [x for x in trials if x['results_metrics']['profit'] > 0] + + logger.info(f"{len(trials)} " + + ("best " if only_best else "") + + ("profitable " if only_profitable else "") + + "epochs found.") + + return trials