class Hyperopt(Backtesting): """ Hyperopt class, this class contains all the logic to run a hyperopt simulation To run a backtest: hyperopt = Hyperopt(config) hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: super().__init__(config) self.custom_hyperopt = HyperOptResolver(self.config).hyperopt # set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days self.target_trades = 600 self.total_tries = config.get('epochs', 0) self.current_best_loss = 100 # max average trade duration in minutes # if eval ends with higher value, we consider it a failed eval self.max_accepted_trade_duration = 300 # This is assumed to be expected avg profit * expected trade count. # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, # self.expected_max_profit = 3.85 # Check that the reported Σ% values do not exceed this! # Note, this is ratio. 3.85 stated above means 385Σ%. self.expected_max_profit = 3.0 # Previous evaluations self.trials_file = TRIALSDATA_PICKLE self.trials: List = [] def get_args(self, params): dimensions = self.hyperopt_space() # 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)}') # 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 def save_trials(self) -> None: """ Save hyperopt trials to file """ if self.trials: logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file) dump(self.trials, self.trials_file) def read_trials(self) -> List: """ Read hyperopt trials file """ logger.info('Reading Trials from \'%s\'', self.trials_file) trials = load(self.trials_file) os.remove(self.trials_file) return trials def log_trials_result(self) -> None: """ Display Best hyperopt result """ results = sorted(self.trials, key=itemgetter('loss')) best_result = results[0] logger.info( 'Best result:\n%s\nwith values:\n', best_result['result'] ) pprint(best_result['params'], indent=4) if 'roi_t1' in best_result['params']: logger.info('ROI table:') pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) def log_results(self, results) -> None: """ Log results if it is better than any previous evaluation """ print_all = self.config.get('print_all', False) if print_all or results['loss'] < self.current_best_loss: # Output human-friendly index here (starting from 1) current = results['current_tries'] + 1 total = results['total_tries'] res = results['result'] loss = results['loss'] self.current_best_loss = results['loss'] log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}' if print_all: print(log_msg) else: print('\n' + log_msg) else: print('.', end='') sys.stdout.flush() def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float: """ Objective function, returns smaller number for more optimal results """ trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) profit_loss = max(0, 1 - total_profit / self.expected_max_profit) duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) result = trade_loss + profit_loss + duration_loss return result def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration """ if space in self.config['spaces'] or 'all' in self.config['spaces']: return True return False def hyperopt_space(self) -> List[Dimension]: """ Return the space to use during Hyperopt """ spaces: List[Dimension] = [] if self.has_space('buy'): spaces += self.custom_hyperopt.indicator_space() if self.has_space('sell'): spaces += self.custom_hyperopt.sell_indicator_space() # Make sure experimental is enabled if 'experimental' not in self.config: self.config['experimental'] = {} self.config['experimental']['use_sell_signal'] = True if self.has_space('roi'): spaces += self.custom_hyperopt.roi_space() if self.has_space('stoploss'): spaces += self.custom_hyperopt.stoploss_space() return spaces def generate_optimizer(self, _params: Dict) -> Dict: params = self.get_args(_params) if self.has_space('roi'): self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) if self.has_space('buy'): self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) elif hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if self.has_space('sell'): self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) elif hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore if self.has_space('stoploss'): self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) min_date, max_date = get_timeframe(processed) results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, 'position_stacking': self.config.get('position_stacking', True), 'start_date': min_date, 'end_date': max_date, } ) result_explanation = self.format_results(results) total_profit = results.profit_percent.sum() trade_count = len(results.index) trade_duration = results.trade_duration.mean() # 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, 'result': result_explanation, } loss = self.calculate_loss(total_profit, trade_count, trade_duration) return { 'loss': loss, 'params': params, 'result': result_explanation, } def format_results(self, results: DataFrame) -> str: """ Return the format result 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}Σ%). Avg duration {duration:5.1f} mins.') def get_optimizer(self, cpu_count) -> Optimizer: return Optimizer( self.hyperopt_space(), 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) ) def run_optimizer_parallel(self, parallel, asked) -> List: return parallel(delayed( wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked) def load_previous_results(self): """ read trials file if we have one """ if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0: self.trials = self.read_trials() logger.info( 'Loaded %d previous evaluations from disk.', len(self.trials) ) def start(self) -> None: timerange = Arguments.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = load_data( datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=self.config['exchange']['pair_whitelist'], ticker_interval=self.ticker_interval, refresh_pairs=self.config.get('refresh_pairs', False), exchange=self.exchange, timerange=timerange ) if not data: logger.critical("No data found. Terminating.") return min_date, max_date = get_timeframe(data) logger.info( 'Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) if self.has_space('buy') or self.has_space('sell'): self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore preprocessed = self.strategy.tickerdata_to_dataframe(data) dump(preprocessed, TICKERDATA_PICKLE) # We don't need exchange instance anymore while running hyperopt self.exchange = None # type: ignore self.load_previous_results() 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}') opt = self.get_optimizer(config_jobs) try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info(f'Effective number of parallel workers used: {jobs}') EVALS = max(self.total_tries // jobs, 1) for i in range(EVALS): asked = opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked) opt.tell(asked, [i['loss'] for i in f_val]) self.trials += f_val for j in range(jobs): current = i * jobs + j self.log_results({ 'loss': f_val[j]['loss'], 'current_tries': current, 'initial_point': current < INITIAL_POINTS, 'total_tries': self.total_tries, 'result': f_val[j]['result'], }) logger.debug(f"Optimizer params: {f_val[j]['params']}") for j in range(jobs): logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}") except KeyboardInterrupt: print('User interrupted..') self.save_trials() self.log_trials_result()
class Hyperopt: """ Hyperopt class, this class contains all the logic to run a hyperopt simulation To run a backtest: hyperopt = Hyperopt(config) hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: self.config = config self.backtesting = Backtesting(self.config) self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperoptloss = HyperOptLossResolver( self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.trials_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_results.pickle') self.tickerdata_pickle = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 if not self.config.get('hyperopt_continue'): self.clean_hyperopt() else: logger.info("Continuing on previous hyperopt results.") # Previous evaluations self.trials: List = [] # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.backtesting.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.backtesting.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] else: logger.debug( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...' ) self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False), if self.has_space('sell'): # Make sure experimental is enabled if 'experimental' not in self.config: self.config['experimental'] = {} self.config['experimental']['use_sell_signal'] = True @staticmethod def get_lock_filename(config) -> str: return str(config['user_data_dir'] / 'hyperopt.lock') def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. """ for f in [self.tickerdata_pickle, self.trials_file]: p = Path(f) if p.is_file(): logger.info(f"Removing `{p}`.") p.unlink() def get_args(self, params): dimensions = self.hyperopt_space() # 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)}' ) # 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 def save_trials(self) -> None: """ Save hyperopt trials to file """ if self.trials: logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file) dump(self.trials, self.trials_file) def read_trials(self) -> List: """ Read hyperopt trials file """ logger.info('Reading Trials from \'%s\'', self.trials_file) trials = load(self.trials_file) self.trials_file.unlink() return trials def log_trials_result(self) -> None: """ Display Best hyperopt result """ 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.config.get('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'): # 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()) if self.has_space('stoploss'): result_dict['stoploss'] = params.get('stoploss') print( rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: if self.has_space('buy'): 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'): 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'): print("ROI table:") pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) if self.has_space('stoploss'): print(f"Stoploss: {params.get('stoploss')}") def log_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 = results['loss'] < self.current_best_loss 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('\n' + log_str) else: print('.', end='') sys.stdout.flush() def format_results_logstring(self, results) -> str: # Output human-friendly index here (starting from 1) current = results['current_epoch'] + 1 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 def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration """ return any(s in self.config['spaces'] for s in [space, 'all']) def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ Return the dimensions in the hyperoptimization space. :param space: Defines hyperspace to return dimensions for. If None, then the self.has_space() will be used to return dimensions for all hyperspaces used. """ spaces: List[Dimension] = [] if space == 'buy' or (space is None and self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces def generate_optimizer(self, _params: Dict) -> 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) if self.has_space('roi'): self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( params) if self.has_space('buy'): self.backtesting.advise_buy = self.custom_hyperopt.buy_strategy_generator( params) if self.has_space('sell'): self.backtesting.advise_sell = self.custom_hyperopt.sell_strategy_generator( params) if self.has_space('stoploss'): self.backtesting.strategy.stoploss = params['stoploss'] processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) results = self.backtesting.backtest({ 'stake_amount': self.config['stake_amount'], 'processed': processed, 'max_open_trades': self.max_open_trades, 'position_stacking': self.position_stacking, 'start_date': min_date, 'end_date': max_date, }) results_explanation = self.format_results(results) trade_count = len(results.index) total_profit = results.profit_abs.sum() # 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) return { 'loss': loss, 'params': params, 'results_explanation': results_explanation, 'total_profit': total_profit, } def format_results(self, results: DataFrame) -> 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}Σ%). Avg duration {duration:5.1f} mins.') def get_optimizer(self, cpu_count) -> Optimizer: return Optimizer(self.hyperopt_space(), 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)) def run_optimizer_parallel(self, parallel, asked) -> List: return parallel( delayed(wrap_non_picklable_objects(self.generate_optimizer))(v) 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)) def start(self) -> None: timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) data = load_data(datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, pairs=self.config['exchange']['pair_whitelist'], ticker_interval=self.backtesting.ticker_interval, refresh_pairs=self.config.get('refresh_pairs', False), exchange=self.backtesting.exchange, timerange=timerange) if not data: logger.critical("No data found. Terminating.") return min_date, max_date = get_timeframe(data) logger.info('Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days) self.backtesting.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange = None # type: ignore self.load_previous_results() 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}') opt = self.get_optimizer(config_jobs) if self.config.get('print_colorized', False): colorama_init(autoreset=True) try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info( f'Effective number of parallel workers used: {jobs}') EVALS = max(self.total_epochs // jobs, 1) for i in range(EVALS): asked = opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked) opt.tell(asked, [v['loss'] for v in f_val]) for j in range(jobs): current = i * jobs + j val = f_val[j] val['current_epoch'] = current val['is_initial_point'] = current < INITIAL_POINTS self.log_results(val) self.trials.append(val) logger.debug(f"Optimizer epoch evaluated: {val}") except KeyboardInterrupt: print('User interrupted..') self.save_trials() self.log_trials_result()
class Hyperopt: """ Hyperopt class, this class contains all the logic to run a hyperopt simulation To run a backtest: hyperopt = Hyperopt(config) hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: self.config = config self.backtesting = Backtesting(self.config) self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperoptloss = HyperOptLossResolver( self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.trials_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_results.pickle') self.tickerdata_pickle = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 if not self.config.get('hyperopt_continue'): self.clean_hyperopt() else: logger.info("Continuing on previous hyperopt results.") self.num_trials_saved = 0 # Previous evaluations self.trials: List = [] # 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 = \ self.custom_hyperopt.populate_indicators # type: ignore if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.backtesting.strategy.advise_buy = \ self.custom_hyperopt.populate_buy_trend # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.backtesting.strategy.advise_sell = \ self.custom_hyperopt.populate_sell_trend # type: ignore # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] else: logger.debug( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...' ) self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False) if self.has_space('sell'): # Make sure use_sell_signal is enabled if 'ask_strategy' not in self.config: 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: return str(config['user_data_dir'] / 'hyperopt.lock') def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. """ for f in [self.tickerdata_pickle, self.trials_file]: p = Path(f) if p.is_file(): logger.info(f"Removing `{p}`.") p.unlink() def _get_params_dict(self, raw_params: List[Any]) -> Dict: dimensions: List[Dimension] = self.dimensions # Ensure the number of dimensions match # the number of parameters in the list. if len(raw_params) != len(dimensions): raise ValueError('Mismatch in number of search-space dimensions.') # 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: """ Save hyperopt trials to file """ num_trials = len(self.trials) if num_trials > self.num_trials_saved: logger.info(f"Saving {num_trials} {plural(num_trials, 'epoch')}.") dump(self.trials, self.trials_file) self.num_trials_saved = num_trials if final: logger.info(f"{num_trials} {plural(num_trials, 'epoch')} " f"saved to '{self.trials_file}'.") @staticmethod def _read_trials(trials_file) -> List: """ Read hyperopt trials file """ logger.info("Reading Trials from '%s'", trials_file) trials = load(trials_file) return trials def _get_params_details(self, params: Dict) -> Dict: """ Return the params for each space """ result: Dict = {} 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'] = { p.name: params.get(p.name) for p in self.hyperopt_space('stoploss') } if self.has_space('trailing'): result['trailing'] = { p.name: params.get(p.name) for p in self.hyperopt_space('trailing') } return result @staticmethod 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.get('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 = {} for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: Hyperopt._params_update_for_json(result_dict, params, s) print( rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:") Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:") Hyperopt._params_pretty_print(params, 'roi', "ROI table:") Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:") Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:") @staticmethod def _params_update_for_json(result_dict, params, space: str): if space in params: space_params = Hyperopt._space_params(params, space) if space in ['buy', 'sell']: result_dict.setdefault('params', {}).update(space_params) elif space == 'roi': # 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 space_params.items()) else: # 'stoploss', 'trailing' result_dict.update(space_params) @staticmethod def _params_pretty_print(params, space: str, header: str): if space in params: space_params = Hyperopt._space_params(params, space, 5) if space == 'stoploss': print(header, space_params.get('stoploss')) else: print(header) pprint(space_params, indent=4) @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: d = params[space] # Round floats to `r` digits after the decimal point if requested return round_dict(d, r) if r else d @staticmethod def is_best_loss(results, current_best_loss) -> bool: return results['loss'] < current_best_loss def print_results(self, results) -> None: """ Log results if it is better than any previous evaluation """ 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 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) @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: """ Tell if the space value is contained in the configuration """ # The 'trailing' space is not included in the 'default' set of spaces if space == 'trailing': return any(s in self.config['spaces'] for s in [space, 'all']) else: return any(s in self.config['spaces'] for s in [space, 'all', 'default']) def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ Return the dimensions in the hyperoptimization space. :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 self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() if space == 'trailing' or (space is None and self.has_space('trailing')): logger.debug("Hyperopt has 'trailing' space") spaces += self.custom_hyperopt.trailing_space() return spaces def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: """ Used Optimize function. Called once per epoch to optimize whatever is configured. Keep this function as optimized as possible! """ params_dict = self._get_params_dict(raw_params) params_details = self._get_params_details(params_dict) if self.has_space('roi'): self.backtesting.strategy.minimal_roi = \ 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_dict) if self.has_space('sell'): self.backtesting.strategy.advise_sell = \ self.custom_hyperopt.sell_strategy_generator(params_dict) if self.has_space('stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] if self.has_space('trailing'): self.backtesting.strategy.trailing_stop = params_dict[ 'trailing_stop'] self.backtesting.strategy.trailing_stop_positive = \ params_dict['trailing_stop_positive'] self.backtesting.strategy.trailing_stop_positive_offset = \ params_dict['trailing_stop_positive_offset'] self.backtesting.strategy.trailing_only_offset_is_reached = \ params_dict['trailing_only_offset_is_reached'] processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) backtesting_results = self.backtesting.backtest({ 'stake_amount': self.config['stake_amount'], 'processed': processed, 'max_open_trades': self.max_open_trades, 'position_stacking': self.position_stacking, 'start_date': min_date, 'end_date': max_date, }) return self._get_results_dict(backtesting_results, min_date, max_date, params_dict, params_details) def _get_results_dict(self, backtesting_results, min_date, max_date, params_dict, params_details): results_metrics = self._calculate_results_metrics(backtesting_results) results_explanation = self._format_results_explanation_string( results_metrics) trade_count = results_metrics['trade_count'] total_profit = results_metrics['total_profit'] # 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. 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_dict': params_dict, 'params_details': params_details, 'results_metrics': results_metrics, 'results_explanation': results_explanation, 'total_profit': total_profit, } 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 """ stake_cur = self.config['stake_currency'] 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: 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), ) def fix_optimizer_models_list(self): """ WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746 This may cease working when skopt updates if implementation of this intrinsic part changes. """ n = len(self.opt.models) - SKOPT_MODELS_MAX_NUM # Keep no more than 2*SKOPT_MODELS_MAX_NUM models in the skopt models list, # remove the old ones. These are actually of no use, the current model # from the estimator is the only one used in the skopt optimizer. # Freqtrade code also does not inspect details of the models. if n >= SKOPT_MODELS_MAX_NUM: logger.debug( f"Fixing skopt models list, removing {n} old items...") del self.opt.models[0:n] def run_optimizer_parallel(self, parallel, asked, i) -> List: return parallel( delayed(wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) @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) if trials[0].get('is_best') is None: raise OperationalException( "The file with Hyperopt results is incompatible with this version " "of Freqtrade and cannot be loaded.") logger.info( f"Loaded {len(trials)} previous evaluations from disk.") return trials def start(self) -> None: data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) min_date, max_date = get_timeframe(data) logger.info('Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days) dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange = None # type: ignore 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: List[Dimension] = self.hyperopt_space() self.opt = self.get_optimizer(self.dimensions, config_jobs) if self.print_colorized: colorama_init(autoreset=True) try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info( f'Effective number of parallel workers used: {jobs}') EVALS = max(self.total_epochs // jobs, 1) for i in range(EVALS): asked = self.opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) self.opt.tell(asked, [v['loss'] for v in f_val]) self.fix_optimizer_models_list() for j in range(jobs): # 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_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) 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.")
class Hyperopt: """ Hyperopt class, this class contains all the logic to run a hyperopt simulation To run a backtest: hyperopt = Hyperopt(config) hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: self.config = config self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.backtesting = Backtesting(self.config) self.custom_hyperoptloss = HyperOptLossResolver( self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.trials_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_results.pickle') self.tickerdata_pickle = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 if not self.config.get('hyperopt_continue'): self.clean_hyperopt() else: logger.info("Continuing on previous hyperopt results.") # 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 = \ self.custom_hyperopt.populate_indicators # type: ignore if hasattr(self.custom_hyperopt, 'populate_buy_trend'): self.backtesting.strategy.advise_buy = \ self.custom_hyperopt.populate_buy_trend # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.backtesting.strategy.advise_sell = \ self.custom_hyperopt.populate_sell_trend # type: ignore # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] else: logger.debug( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...' ) self.max_open_trades = 0 self.position_stacking = self.config.get('position_stacking', False) if self.has_space('sell'): # Make sure use_sell_signal is enabled if 'ask_strategy' not in self.config: self.config['ask_strategy'] = {} self.config['ask_strategy']['use_sell_signal'] = True @staticmethod def get_lock_filename(config) -> str: return str(config['user_data_dir'] / 'hyperopt.lock') def clean_hyperopt(self): """ Remove hyperopt pickle files to restart hyperopt. """ for f in [self.tickerdata_pickle, self.trials_file]: p = Path(f) if p.is_file(): logger.info(f"Removing `{p}`.") p.unlink() def get_args(self, params): dimensions = 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)}' ) # 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 def save_trials(self, final: bool = False) -> None: """ Save hyperopt trials to file """ num_trials = len(self.trials) if num_trials > self.num_trials_saved: logger.info(f"Saving {num_trials} {plural(num_trials, 'epoch')}.") dump(self.trials, self.trials_file) self.num_trials_saved = num_trials if final: logger.info(f"{num_trials} {plural(num_trials, 'epoch')} " f"saved to '{self.trials_file}'.") def read_trials(self) -> List: """ Read hyperopt trials file """ logger.info("Reading Trials from '%s'", self.trials_file) trials = load(self.trials_file) self.trials_file.unlink() return trials def log_trials_result(self) -> None: """ Display Best hyperopt result """ # 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 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.config.get('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'): # 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()) if self.has_space('stoploss'): result_dict['stoploss'] = params.get('stoploss') print( rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: if self.has_space('buy'): 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'): 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'): 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'): # Also round to 5 digits after the decimal point print(f"Stoploss: {round(params.get('stoploss'), 5)}") def is_best(self, results) -> bool: return results['loss'] < self.current_best_loss def log_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: 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}') 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 def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration """ return any(s in self.config['spaces'] for s in [space, 'all']) def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ Return the dimensions in the hyperoptimization space. :param space: Defines hyperspace to return dimensions for. If None, then the self.has_space() will be used to return dimensions for all hyperspaces used. """ spaces: List[Dimension] = [] if space == 'buy' or (space is None and self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces def generate_optimizer(self, _params: Dict, 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) if self.has_space('roi'): self.backtesting.strategy.minimal_roi = \ self.custom_hyperopt.generate_roi_table(params) if self.has_space('buy'): self.backtesting.strategy.advise_buy = \ self.custom_hyperopt.buy_strategy_generator(params) if self.has_space('sell'): self.backtesting.strategy.advise_sell = \ self.custom_hyperopt.sell_strategy_generator(params) if self.has_space('stoploss'): self.backtesting.strategy.stoploss = params['stoploss'] processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) results = self.backtesting.backtest({ 'stake_amount': self.config['stake_amount'], 'processed': processed, 'max_open_trades': self.max_open_trades, 'position_stacking': self.position_stacking, 'start_date': min_date, 'end_date': max_date, }) results_explanation = self.format_results(results) trade_count = len(results.index) total_profit = results.profit_abs.sum() # 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) return { 'loss': loss, 'params': params, 'results_explanation': results_explanation, 'total_profit': total_profit, } def format_results(self, results: DataFrame) -> 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.').encode( locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions, 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)) def fix_optimizer_models_list(self): """ WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746 This may cease working when skopt updates if implementation of this intrinsic part changes. """ n = len(self.opt.models) - SKOPT_MODELS_MAX_NUM # Keep no more than 2*SKOPT_MODELS_MAX_NUM models in the skopt models list, # remove the old ones. These are actually of no use, the current model # from the estimator is the only one used in the skopt optimizer. # Freqtrade code also does not inspect details of the models. if n >= SKOPT_MODELS_MAX_NUM: logger.debug( f"Fixing skopt models list, removing {n} old items...") del self.opt.models[0:n] def run_optimizer_parallel(self, parallel, asked, i) -> List: 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)) def start(self) -> None: data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) min_date, max_date = get_timeframe(data) logger.info('Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days) dump(preprocessed, self.tickerdata_pickle) # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange = None # type: ignore self.load_previous_results() 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.opt = self.get_optimizer(self.dimensions, config_jobs) if self.config.get('print_colorized', False): colorama_init(autoreset=True) try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() logger.info( f'Effective number of parallel workers used: {jobs}') EVALS = max(self.total_epochs // jobs, 1) for i in range(EVALS): asked = self.opt.ask(n_points=jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) 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) 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) self.trials.append(val) if is_best or current % 100 == 0: self.save_trials() except KeyboardInterrupt: print('User interrupted..') self.save_trials(final=True) self.log_trials_result()