Пример #1
0
def test_hyperoptresolver_noname(default_conf):
    default_conf['hyperopt'] = ''
    with pytest.raises(
            OperationalException,
            match="No Hyperopt set. Please use `--hyperopt` to specify "
            "the Hyperopt class to use."):
        HyperOptResolver.load_hyperopt(default_conf)
Пример #2
0
    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)
Пример #3
0
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
    patched_configuration_load_config_file(mocker, default_conf)

    hyperopt = DefaultHyperOpt
    delattr(hyperopt, 'populate_indicators')
    delattr(hyperopt, 'populate_buy_trend')
    delattr(hyperopt, 'populate_sell_trend')
    mocker.patch(
        'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object',
        MagicMock(return_value=hyperopt(default_conf)))
    default_conf.update({'hyperopt': 'DefaultHyperOpt'})
    x = HyperOptResolver.load_hyperopt(default_conf)
    assert not hasattr(x, 'populate_indicators')
    assert not hasattr(x, 'populate_buy_trend')
    assert not hasattr(x, 'populate_sell_trend')
    assert log_has(
        "Hyperopt class does not provide populate_indicators() method. "
        "Using populate_indicators from the strategy.", caplog)
    assert log_has(
        "Hyperopt class does not provide populate_sell_trend() method. "
        "Using populate_sell_trend from the strategy.", caplog)
    assert log_has(
        "Hyperopt class does not provide populate_buy_trend() method. "
        "Using populate_buy_trend from the strategy.", caplog)
    assert hasattr(x, "ticker_interval")  # DEPRECATED
    assert hasattr(x, "timeframe")
Пример #4
0
    def __init__(self, config: Dict[str, Any]) -> None:
        self.config = config

        self.backtesting = Backtesting(self.config)

        if not self.config.get('hyperopt'):
            self.custom_hyperopt = HyperOptAuto(self.config)
        else:
            self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
        self.custom_hyperopt.strategy = self.backtesting.strategy

        self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
        self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
        time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        strategy = str(self.config['strategy'])
        self.results_file = (self.config['user_data_dir'] /
                             'hyperopt_results' /
                             f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
        self.data_pickle_file = (self.config['user_data_dir'] /
                                 'hyperopt_results' / 'hyperopt_tickerdata.pkl')
        self.total_epochs = config.get('epochs', 0)

        self.current_best_loss = 100

        self.clean_hyperopt()

        self.num_epochs_saved = 0

        # Previous evaluations
        self.epochs: 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 = (  # type: ignore
                self.custom_hyperopt.populate_indicators)  # type: ignore
        if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
            self.backtesting.strategy.advise_buy = (  # type: ignore
                self.custom_hyperopt.populate_buy_trend)  # type: ignore
        if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
            self.backtesting.strategy.advise_sell = (  # type: ignore
                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.hyperopt_table_header = 0
        self.print_colorized = self.config.get('print_colorized', False)
        self.print_json = self.config.get('print_json', False)
Пример #5
0
    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 = []
Пример #6
0
    def __init__(self, config: Dict[str, Any]) -> None:
        super().__init__(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.total_tries = 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_file = TRIALSDATA_PICKLE
        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.advise_buy = self.custom_hyperopt.populate_buy_trend  # type: ignore

        if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
            self.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),
Пример #7
0
def start_list_hyperopts(args: Dict[str, Any]) -> None:
    """
    Print files with HyperOpt custom classes available in the directory
    """
    from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver

    config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)

    directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS))
    hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column'])
    # Sort alphabetically
    hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name'])

    if args['print_one_column']:
        print('\n'.join([s['name'] for s in hyperopt_objs]))
    else:
        _print_objs_tabular(hyperopt_objs, config.get('print_colorized', False))
Пример #8
0
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
    patched_configuration_load_config_file(mocker, default_conf)

    hyperopts = DefaultHyperOpts
    delattr(hyperopts, 'populate_buy_trend')
    delattr(hyperopts, 'populate_sell_trend')
    mocker.patch(
        'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
        MagicMock(return_value=hyperopts)
    )
    x = HyperOptResolver(default_conf, ).hyperopt
    assert not hasattr(x, 'populate_buy_trend')
    assert not hasattr(x, 'populate_sell_trend')
    assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
                   "Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
    assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
                   "Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples)
    assert hasattr(x, "ticker_interval")
Пример #9
0
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
    patched_configuration_load_config_file(mocker, default_conf)

    hyperopt = DefaultHyperOpt
    delattr(hyperopt, 'populate_buy_trend')
    delattr(hyperopt, 'populate_sell_trend')
    mocker.patch(
        'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
        MagicMock(return_value=hyperopt(default_conf)))
    x = HyperOptResolver(default_conf, ).hyperopt
    assert not hasattr(x, 'populate_buy_trend')
    assert not hasattr(x, 'populate_sell_trend')
    assert log_has(
        "Hyperopt class does not provide populate_sell_trend() method. "
        "Using populate_sell_trend from the strategy.", caplog)
    assert log_has(
        "Hyperopt class does not provide populate_buy_trend() method. "
        "Using populate_buy_trend from the strategy.", caplog)
    assert hasattr(x, "ticker_interval")
Пример #10
0
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()
Пример #11
0
def test_hyperoptresolver_wrongname(default_conf) -> None:
    default_conf.update({'hyperopt': "NonExistingHyperoptClass"})

    with pytest.raises(OperationalException,
                       match=r'Impossible to load Hyperopt.*'):
        HyperOptResolver.load_hyperopt(default_conf)
Пример #12
0
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.")
Пример #13
0
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()
Пример #14
0
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()