stable/freqtrade/optimize/hyperopt.py

730 lines
31 KiB
Python
Raw Normal View History

# pragma pylint: disable=too-many-instance-attributes, pointless-string-statement
"""
This module contains the hyperopt logic
"""
2019-11-06 18:33:15 +00:00
import locale
import logging
import random
import warnings
from math import ceil
2019-08-15 18:39:04 +00:00
from collections import OrderedDict
from operator import itemgetter
2019-01-06 13:47:38 +00:00
from pathlib import Path
from pprint import pformat
2019-08-02 19:22:58 +00:00
from typing import Any, Dict, List, Optional
2019-08-15 18:39:04 +00:00
import rapidjson
2019-08-09 11:48:57 +00:00
from colorama import Fore, Style
2019-11-06 18:33:15 +00:00
from joblib import (Parallel, cpu_count, delayed, dump, load,
wrap_non_picklable_objects)
from pandas import DataFrame, json_normalize, isna
import progressbar
import tabulate
2020-03-12 01:26:41 +00:00
from os import path
import io
2018-06-18 19:40:36 +00:00
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.history import get_timerange
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural, round_dict
2018-03-02 15:22:00 +00:00
from freqtrade.optimize.backtesting import Backtesting
2019-08-14 10:25:49 +00:00
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
2019-11-06 18:33:15 +00:00
from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
HyperOptResolver)
2019-12-10 15:10:51 +00:00
# Suppress scikit-learn FutureWarnings from skopt
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
from skopt import Optimizer
from skopt.space import Dimension
progressbar.streams.wrap_stderr()
progressbar.streams.wrap_stdout()
2018-03-25 19:37:14 +00:00
logger = logging.getLogger(__name__)
2019-05-10 07:54:44 +00:00
INITIAL_POINTS = 30
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt model queue, to optimize memory consumption
SKOPT_MODEL_QUEUE_SIZE = 10
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
2018-03-25 19:37:14 +00:00
class Hyperopt:
"""
Hyperopt class, this class contains all the logic to run a hyperopt simulation
To run a backtest:
hyperopt = Hyperopt(config)
hyperopt.start()
"""
2020-01-31 21:37:05 +00:00
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
2019-09-18 19:57:17 +00:00
self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
2019-07-16 04:27:23 +00:00
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
2020-04-28 19:56:19 +00:00
self.results_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.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
2019-07-16 03:50:27 +00:00
if not self.config.get('hyperopt_continue'):
self.clean_hyperopt()
2019-07-16 03:50:27 +00:00
else:
logger.info("Continuing on previous hyperopt results.")
2020-04-28 19:56:19 +00:00
self.num_epochs_saved = 0
# Previous evaluations
2020-04-28 19:56:19 +00:00
self.epochs: List = []
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
2019-09-16 18:22:07 +00:00
if hasattr(self.custom_hyperopt, 'populate_indicators'):
self.backtesting.strategy.advise_indicators = \
2020-01-31 21:37:05 +00:00
self.custom_hyperopt.populate_indicators # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
2019-09-18 19:57:17 +00:00
self.backtesting.strategy.advise_buy = \
2020-01-31 21:37:05 +00:00
self.custom_hyperopt.populate_buy_trend # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
2019-09-18 19:57:17 +00:00
self.backtesting.strategy.advise_sell = \
2020-01-31 21:37:05 +00:00
self.custom_hyperopt.populate_sell_trend # type: ignore
2019-08-02 19:22:58 +00:00
# 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
2019-09-25 00:41:22 +00:00
self.position_stacking = self.config.get('position_stacking', False)
2019-08-01 20:57:26 +00:00
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
2019-08-01 20:57:26 +00:00
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)
2019-07-21 14:07:06 +00:00
@staticmethod
2020-02-02 04:00:40 +00:00
def get_lock_filename(config: Dict[str, Any]) -> str:
2019-07-21 14:07:06 +00:00
return str(config['user_data_dir'] / 'hyperopt.lock')
2020-02-02 04:00:40 +00:00
def clean_hyperopt(self) -> None:
"""
Remove hyperopt pickle files to restart hyperopt.
"""
2020-04-28 19:56:19 +00:00
for f in [self.data_pickle_file, self.results_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:
2019-09-16 18:22:07 +00:00
dimensions: List[Dimension] = self.dimensions
2019-09-16 18:22:07 +00:00
2018-06-19 06:09:54 +00:00
# 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.')
2018-06-19 06:09:54 +00:00
# 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)}
2018-06-19 06:09:54 +00:00
2020-04-28 19:56:19 +00:00
def _save_results(self) -> None:
"""
2020-04-28 19:56:19 +00:00
Save hyperopt results to file
"""
2020-04-28 19:56:19 +00:00
num_epochs = len(self.epochs)
if num_epochs > self.num_epochs_saved:
logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.")
dump(self.epochs, self.results_file)
self.num_epochs_saved = num_epochs
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.")
@staticmethod
2020-04-28 19:56:19 +00:00
def _read_results(results_file: Path) -> List:
"""
2020-04-28 19:56:19 +00:00
Read hyperopt results from file
"""
2020-04-28 19:56:19 +00:00
logger.info("Reading epochs from '%s'", results_file)
data = load(results_file)
return data
def _get_params_details(self, params: Dict) -> Dict:
"""
Return the params for each space
"""
result: Dict = {}
2019-11-23 08:32:33 +00:00
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')}
2019-12-04 22:08:38 +00:00
if self.has_space('trailing'):
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
2019-08-15 18:39:04 +00:00
return result
2019-11-23 08:32:33 +00:00
2019-12-05 20:29:31 +00:00
@staticmethod
2020-02-02 04:00:40 +00:00
def print_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
"""
Display details of the hyperopt result
"""
2019-11-27 19:52:43 +00:00
params = results.get('params_details', {})
# Default header string
if header_str is None:
header_str = "Best result"
2019-12-01 13:15:00 +00:00
if not no_header:
explanation_str = Hyperopt._format_explanation_string(results, total_epochs)
print(f"\n{header_str}:\n\n{explanation_str}\n")
2019-08-15 18:39:04 +00:00
if print_json:
2019-08-15 20:13:46 +00:00
result_dict: Dict = {}
2019-12-01 13:15:00 +00:00
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
Hyperopt._params_update_for_json(result_dict, params, s)
2019-12-01 13:15:00 +00:00
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
2019-11-07 22:55:14 +00:00
2019-12-01 13:15:00 +00:00
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:")
2019-12-04 22:08:38 +00:00
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
2020-02-02 04:00:40 +00:00
def _params_update_for_json(result_dict, params, space: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space)
2019-12-01 13:15:00 +00:00
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
2020-06-13 14:12:37 +00:00
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
2019-08-15 18:39:04 +00:00
# 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.
2019-08-15 20:13:46 +00:00
result_dict['minimal_roi'] = OrderedDict(
2019-12-01 13:15:00 +00:00
(str(k), v) for k, v in space_params.items()
2019-08-15 20:13:46 +00:00
)
2019-12-01 13:15:00 +00:00
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
2019-11-07 22:55:14 +00:00
@staticmethod
2020-02-02 04:00:40 +00:00
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space, 5)
2020-06-13 14:12:37 +00:00
params_result = f"\n# {header}\n"
if space == 'stoploss':
2020-06-13 14:12:37 +00:00
params_result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
2020-06-13 14:12:37 +00:00
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
2020-06-13 15:54:54 +00:00
minimal_roi_result = rapidjson.dumps(
OrderedDict(
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
2020-06-13 14:12:37 +00:00
params_result += f"minimal_roi = {minimal_roi_result}"
else:
2020-06-13 14:12:37 +00:00
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
params_result = params_result.replace("\n", "\n ")
print(params_result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
@staticmethod
2020-02-02 04:00:40 +00:00
def is_best_loss(results, current_best_loss: float) -> bool:
return results['loss'] < current_best_loss
2019-11-23 08:32:33 +00:00
def print_results(self, results) -> None:
"""
Log results if it is better than any previous evaluation
"""
is_best = results['is_best']
2019-11-23 08:32:33 +00:00
if self.print_all or is_best:
print(
self.get_result_table(
2020-03-11 21:30:36 +00:00
self.config, results, self.total_epochs,
self.print_all, self.print_colorized,
self.hyperopt_table_header
)
)
self.hyperopt_table_header = 2
@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}")
@staticmethod
2020-03-11 21:30:36 +00:00
def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
print_colorized: bool, remove_header: int) -> str:
"""
Log result table
"""
if not results:
2020-03-11 21:30:36 +00:00
return ''
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
trials['is_profit'] = False
2020-04-24 18:57:29 +00:00
trials.loc[trials['is_initial_point'], 'Best'] = '* '
trials.loc[trials['is_best'], 'Best'] = 'Best'
2020-04-24 18:57:29 +00:00
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str)
trials['Epoch'] = trials['Epoch'].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply(
2020-03-10 07:38:37 +00:00
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Avg duration'] = trials['Avg duration'].apply(
2020-03-10 07:38:37 +00:00
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
2020-03-04 19:51:09 +00:00
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
trials['Profit'] = trials.apply(
2020-03-04 19:51:09 +00:00
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
2020-03-04 19:51:09 +00:00
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
axis=1
)
trials = trials.drop(columns=['Total profit'])
if print_colorized:
for i in range(len(trials)):
if trials.loc[i]['is_profit']:
2020-03-04 19:51:09 +00:00
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best:
2020-03-04 19:51:09 +00:00
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
if remove_header > 0:
table = tabulate.tabulate(
2020-03-04 19:51:09 +00:00
trials.to_dict(orient='list'), tablefmt='orgtbl',
headers='keys', stralign="right"
)
table = table.split("\n", remove_header)[remove_header]
elif remove_header < 0:
table = tabulate.tabulate(
2020-03-04 19:51:09 +00:00
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
table = "\n".join(table.split("\n")[0:remove_header])
else:
table = tabulate.tabulate(
2020-03-04 19:51:09 +00:00
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
2020-03-11 21:30:36 +00:00
return table
@staticmethod
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
2020-03-09 17:53:30 +00:00
csv_file: str) -> None:
"""
Log result to csv-file
"""
if not results:
return
2020-03-08 22:00:21 +00:00
# Verification for overwrite
2020-03-09 17:53:30 +00:00
if path.isfile(csv_file):
2020-04-28 14:33:07 +00:00
logger.error(f"CSV file already exists: {csv_file}")
return
try:
io.open(csv_file, 'w+').close()
except IOError:
2020-04-28 14:33:07 +00:00
logger.error(f"Failed to create CSV file: {csv_file}")
return
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
2020-05-03 14:54:42 +00:00
2020-05-03 15:29:56 +00:00
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
2020-05-03 14:54:42 +00:00
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics]
2020-05-03 15:29:56 +00:00
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
2020-05-03 14:54:42 +00:00
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best'
2020-04-25 09:49:14 +00:00
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Epoch'] = trials['Epoch'].astype(str)
trials['Trades'] = trials['Trades'].astype(str)
trials['Total profit'] = trials['Total profit'].apply(
2020-03-08 21:41:05 +00:00
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
)
trials['Profit'] = trials['Profit'].apply(
2020-03-08 21:41:05 +00:00
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
)
trials['Avg profit'] = trials['Avg profit'].apply(
2020-03-10 07:38:37 +00:00
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
)
trials['Avg duration'] = trials['Avg duration'].apply(
2020-03-10 07:38:37 +00:00
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply(
2020-03-08 21:41:05 +00:00
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
2020-04-28 14:33:07 +00:00
logger.info(f"CSV file created: {csv_file}")
2018-03-17 21:43:36 +00:00
def has_space(self, space: str) -> bool:
"""
2019-11-07 22:55:14 +00:00
Tell if the space value is contained in the configuration
"""
2019-11-07 22:55:14 +00:00
# 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'])
2019-08-02 19:22:58 +00:00
def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
"""
2019-08-03 07:20:20 +00:00
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
2019-08-02 19:22:58 +00:00
for all hyperspaces used.
"""
spaces: List[Dimension] = []
2019-11-07 22:55:14 +00:00
2019-08-02 19:22:58 +00:00
if space == 'buy' or (space is None and self.has_space('buy')):
2019-08-01 20:57:26 +00:00
logger.debug("Hyperopt has 'buy' space")
2018-11-07 18:46:04 +00:00
spaces += self.custom_hyperopt.indicator_space()
2019-11-07 22:55:14 +00:00
2019-08-02 19:22:58 +00:00
if space == 'sell' or (space is None and self.has_space('sell')):
2019-08-01 20:57:26 +00:00
logger.debug("Hyperopt has 'sell' space")
2019-01-06 09:16:30 +00:00
spaces += self.custom_hyperopt.sell_indicator_space()
2019-11-07 22:55:14 +00:00
2019-08-02 19:22:58 +00:00
if space == 'roi' or (space is None and self.has_space('roi')):
2019-08-01 20:57:26 +00:00
logger.debug("Hyperopt has 'roi' space")
2018-11-07 18:46:04 +00:00
spaces += self.custom_hyperopt.roi_space()
2019-11-07 22:55:14 +00:00
2019-08-02 19:22:58 +00:00
if space == 'stoploss' or (space is None and self.has_space('stoploss')):
2019-08-01 20:57:26 +00:00
logger.debug("Hyperopt has 'stoploss' space")
2018-11-07 18:46:04 +00:00
spaces += self.custom_hyperopt.stoploss_space()
2019-11-07 22:55:14 +00:00
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
2017-12-26 08:08:10 +00:00
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'):
2019-09-18 19:57:17 +00:00
self.backtesting.strategy.minimal_roi = \
2020-01-31 21:37:05 +00:00
self.custom_hyperopt.generate_roi_table(params_dict)
if self.has_space('buy'):
2019-09-18 19:57:17 +00:00
self.backtesting.strategy.advise_buy = \
2020-01-31 21:37:05 +00:00
self.custom_hyperopt.buy_strategy_generator(params_dict)
2019-01-06 09:16:30 +00:00
if self.has_space('sell'):
2019-09-18 19:57:17 +00:00
self.backtesting.strategy.advise_sell = \
2020-01-31 21:37:05 +00:00
self.custom_hyperopt.sell_strategy_generator(params_dict)
2019-01-06 09:16:30 +00:00
if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params_dict['stoploss']
2019-11-07 22:55:14 +00:00
if self.has_space('trailing'):
d = self.custom_hyperopt.generate_trailing_params(params_dict)
self.backtesting.strategy.trailing_stop = d['trailing_stop']
self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive']
2019-11-07 22:55:14 +00:00
self.backtesting.strategy.trailing_stop_positive_offset = \
d['trailing_stop_positive_offset']
2019-11-07 22:55:14 +00:00
self.backtesting.strategy.trailing_only_offset_is_reached = \
d['trailing_only_offset_is_reached']
2019-11-07 22:55:14 +00:00
processed = load(self.data_pickle_file)
2019-07-14 17:56:17 +00:00
2019-12-17 22:06:03 +00:00
min_date, max_date = get_timerange(processed)
2019-07-14 17:56:17 +00:00
backtesting_results = self.backtesting.backtest(
2020-01-31 21:37:05 +00:00
processed=processed,
stake_amount=self.config['stake_amount'],
start_date=min_date,
end_date=max_date,
max_open_trades=self.max_open_trades,
position_stacking=self.position_stacking,
)
2019-11-27 19:52:43 +00:00
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)
2019-05-01 12:27:58 +00:00
# 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)
2018-06-19 18:57:42 +00:00
return {
'loss': loss,
'params_dict': params_dict,
'params_details': params_details,
'results_metrics': results_metrics,
'results_explanation': results_explanation,
2019-08-03 16:09:42 +00:00
'total_profit': total_profit,
2018-06-19 18:57:42 +00:00
}
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}%). "
2019-12-11 06:12:37 +00:00
f"Avg duration {results_metrics['duration']:5.1f} min."
2019-11-06 18:33:15 +00:00
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8')
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
2018-06-24 12:27:53 +00:00
return Optimizer(
2019-09-16 18:22:07 +00:00
dimensions,
2018-06-24 12:27:53 +00:00
base_estimator="ET",
acq_optimizer="auto",
2019-05-10 07:54:44 +00:00
n_initial_points=INITIAL_POINTS,
acq_optimizer_kwargs={'n_jobs': cpu_count},
random_state=self.random_state,
model_queue_size=SKOPT_MODEL_QUEUE_SIZE,
2018-06-24 12:27:53 +00:00
)
def run_optimizer_parallel(self, parallel, asked, i) -> List:
2018-11-20 16:43:49 +00:00
return parallel(delayed(
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
2018-06-24 12:27:53 +00:00
@staticmethod
2020-04-28 19:56:19 +00:00
def load_previous_results(results_file: Path) -> List:
"""
Load data for epochs from the file if we have one
"""
2020-04-28 19:56:19 +00:00
epochs: List = []
if results_file.is_file() and results_file.stat().st_size > 0:
epochs = Hyperopt._read_results(results_file)
# Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
2019-12-05 20:29:31 +00:00
raise OperationalException(
2020-01-31 21:37:05 +00:00
"The file with Hyperopt results is incompatible with this version "
"of Freqtrade and cannot be loaded.")
2020-04-28 19:56:19 +00:00
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
return epochs
def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1)
2018-03-17 21:43:36 +00:00
def start(self) -> None:
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
logger.info(f"Using optimizer random state: {self.random_state}")
self.hyperopt_table_header = -1
data, timerange = self.backtesting.load_bt_data()
2020-04-07 08:42:15 +00:00
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
2020-04-07 08:44:18 +00:00
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange)
2019-12-17 22:06:03 +00:00
min_date, max_date = get_timerange(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.data_pickle_file)
# We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore
2020-04-28 19:56:19 +00:00
self.epochs = self.load_previous_results(self.results_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}')
2018-06-21 11:59:36 +00:00
self.dimensions: List[Dimension] = self.hyperopt_space()
2019-09-16 18:22:07 +00:00
self.opt = self.get_optimizer(self.dimensions, 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}')
2020-03-11 21:30:36 +00:00
# Define progressbar
if self.print_colorized:
widgets = [
' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs),
' (', progressbar.Percentage(), ')] ',
progressbar.Bar(marker=progressbar.AnimatedMarker(
fill='\N{FULL BLOCK}',
fill_wrap=Fore.GREEN + '{}' + Fore.RESET,
marker_wrap=Style.BRIGHT + '{}' + Style.RESET_ALL,
)),
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
]
else:
widgets = [
' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs),
' (', progressbar.Percentage(), ')] ',
progressbar.Bar(marker=progressbar.AnimatedMarker(
fill='\N{FULL BLOCK}',
)),
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
]
with progressbar.ProgressBar(
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
widgets=widgets
) as pbar:
EVALS = ceil(self.total_epochs / jobs)
for i in range(EVALS):
# Correct the number of epochs to be processed for the last
# iteration (should not exceed self.total_epochs in total)
n_rest = (i + 1) * jobs - self.total_epochs
current_jobs = jobs - n_rest if n_rest > 0 else jobs
asked = self.opt.ask(n_points=current_jobs)
f_val = self.run_optimizer_parallel(parallel, asked, i)
self.opt.tell(asked, [v['loss'] for v in f_val])
# Calculate progressbar outputs
for j, val in enumerate(f_val):
# Use human-friendly indexes here (starting from 1)
current = i * jobs + j + 1
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']
2020-04-28 19:56:19 +00:00
self.epochs.append(val)
# Save results after each best epoch and every 100 epochs
if is_best or current % 100 == 0:
2020-04-28 19:56:19 +00:00
self._save_results()
pbar.update(current)
2020-03-10 19:30:36 +00:00
except KeyboardInterrupt:
print('User interrupted..')
2020-04-28 19:56:19 +00:00
self._save_results()
logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.")
2020-04-28 19:56:19 +00:00
if self.epochs:
sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
best_epoch = sorted_epochs[0]
self.print_epoch_details(best_epoch, 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.")