ruff format: hyperopt

This commit is contained in:
Matthias
2024-05-12 17:16:02 +02:00
parent f1ef537dfa
commit da7addcd98
6 changed files with 630 additions and 496 deletions
+218 -173
View File
@@ -93,30 +93,36 @@ class Hyperopt:
self.backtesting = Backtesting(self.config) self.backtesting = Backtesting(self.config)
self.pairlist = self.backtesting.pairlists.whitelist self.pairlist = self.backtesting.pairlists.whitelist
self.custom_hyperopt: HyperOptAuto self.custom_hyperopt: HyperOptAuto
self.analyze_per_epoch = self.config.get('analyze_per_epoch', False) self.analyze_per_epoch = self.config.get("analyze_per_epoch", False)
HyperoptStateContainer.set_state(HyperoptState.STARTUP) HyperoptStateContainer.set_state(HyperoptState.STARTUP)
if not self.config.get('hyperopt'): if not self.config.get("hyperopt"):
self.custom_hyperopt = HyperOptAuto(self.config) self.custom_hyperopt = HyperOptAuto(self.config)
else: else:
raise OperationalException( raise OperationalException(
"Using separate Hyperopt files has been removed in 2021.9. Please convert " "Using separate Hyperopt files has been removed in 2021.9. Please convert "
"your existing Hyperopt file to the new Hyperoptable strategy interface") "your existing Hyperopt file to the new Hyperoptable strategy interface"
)
self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.backtesting._set_strategy(self.backtesting.strategylist[0])
self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperopt.strategy = self.backtesting.strategy
self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__) self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__)
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss( self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
self.config) self.config
)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
strategy = str(self.config['strategy']) strategy = str(self.config["strategy"])
self.results_file: Path = (self.config['user_data_dir'] / 'hyperopt_results' / self.results_file: Path = (
f'strategy_{strategy}_{time_now}.fthypt') self.config["user_data_dir"]
self.data_pickle_file = (self.config['user_data_dir'] / / "hyperopt_results"
'hyperopt_results' / 'hyperopt_tickerdata.pkl') / f"strategy_{strategy}_{time_now}.fthypt"
self.total_epochs = config.get('epochs', 0) )
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.current_best_loss = 100
@@ -127,24 +133,23 @@ class Hyperopt:
self.current_best_epoch: Optional[Dict[str, Any]] = None self.current_best_epoch: Optional[Dict[str, Any]] = None
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if not self.config.get('use_max_market_positions', True): if not self.config.get("use_max_market_positions", True):
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') logger.debug("Ignoring max_open_trades (--disable-max-market-positions was used) ...")
self.backtesting.strategy.max_open_trades = float('inf') self.backtesting.strategy.max_open_trades = float("inf")
config.update({'max_open_trades': self.backtesting.strategy.max_open_trades}) config.update({"max_open_trades": self.backtesting.strategy.max_open_trades})
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, "sell"):
# Make sure use_exit_signal is enabled # Make sure use_exit_signal is enabled
self.config['use_exit_signal'] = True self.config["use_exit_signal"] = True
self.print_all = self.config.get('print_all', False) self.print_all = self.config.get("print_all", False)
self.hyperopt_table_header = 0 self.hyperopt_table_header = 0
self.print_colorized = self.config.get('print_colorized', False) self.print_colorized = self.config.get("print_colorized", False)
self.print_json = self.config.get('print_json', False) self.print_json = self.config.get("print_json", False)
@staticmethod @staticmethod
def get_lock_filename(config: Config) -> str: def get_lock_filename(config: Config) -> str:
return str(config["user_data_dir"] / "hyperopt.lock")
return str(config['user_data_dir'] / 'hyperopt.lock')
def clean_hyperopt(self) -> None: def clean_hyperopt(self) -> None:
""" """
@@ -163,16 +168,15 @@ class Hyperopt:
to pickle as value. to pickle as value.
""" """
for modules in bases: for modules in bases:
if modules.__name__ != 'IStrategy': if modules.__name__ != "IStrategy":
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
self.hyperopt_pickle_magic(modules.__bases__) self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
# Ensure the number of dimensions match # Ensure the number of dimensions match
# the number of parameters in the list. # the number of parameters in the list.
if len(raw_params) != len(dimensions): if len(raw_params) != len(dimensions):
raise ValueError('Mismatch in number of search-space dimensions.') raise ValueError("Mismatch in number of search-space dimensions.")
# Return a dict where the keys are the names of the dimensions # Return a dict where the keys are the names of the dimensions
# and the values are taken from the list of parameters. # and the values are taken from the list of parameters.
@@ -186,18 +190,23 @@ class Hyperopt:
:param epoch: result dictionary for this epoch. :param epoch: result dictionary for this epoch.
""" """
epoch[FTHYPT_FILEVERSION] = 2 epoch[FTHYPT_FILEVERSION] = 2
with self.results_file.open('a') as f: with self.results_file.open("a") as f:
rapidjson.dump(epoch, f, default=hyperopt_serializer, rapidjson.dump(
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN) epoch,
f,
default=hyperopt_serializer,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN,
)
f.write("\n") f.write("\n")
self.num_epochs_saved += 1 self.num_epochs_saved += 1
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " logger.debug(
f"saved to '{self.results_file}'.") f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'."
)
# Store hyperopt filename # Store hyperopt filename
latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN) latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, file_dump_json(latest_filename, {"latest_hyperopt": str(self.results_file.name)}, log=False)
log=False)
def _get_params_details(self, params: Dict) -> Dict: def _get_params_details(self, params: Dict) -> Dict:
""" """
@@ -205,23 +214,26 @@ class Hyperopt:
""" """
result: Dict = {} result: Dict = {}
if HyperoptTools.has_space(self.config, 'buy'): if HyperoptTools.has_space(self.config, "buy"):
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space} result["buy"] = {p.name: params.get(p.name) for p in self.buy_space}
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, "sell"):
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} result["sell"] = {p.name: params.get(p.name) for p in self.sell_space}
if HyperoptTools.has_space(self.config, 'protection'): if HyperoptTools.has_space(self.config, "protection"):
result['protection'] = {p.name: params.get(p.name) for p in self.protection_space} result["protection"] = {p.name: params.get(p.name) for p in self.protection_space}
if HyperoptTools.has_space(self.config, 'roi'): if HyperoptTools.has_space(self.config, "roi"):
result['roi'] = {str(k): v for k, v in result["roi"] = {
self.custom_hyperopt.generate_roi_table(params).items()} str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()
if HyperoptTools.has_space(self.config, 'stoploss'): }
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} if HyperoptTools.has_space(self.config, "stoploss"):
if HyperoptTools.has_space(self.config, 'trailing'): result["stoploss"] = {p.name: params.get(p.name) for p in self.stoploss_space}
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) if HyperoptTools.has_space(self.config, "trailing"):
if HyperoptTools.has_space(self.config, 'trades'): result["trailing"] = self.custom_hyperopt.generate_trailing_params(params)
result['max_open_trades'] = { if HyperoptTools.has_space(self.config, "trades"):
'max_open_trades': self.backtesting.strategy.max_open_trades result["max_open_trades"] = {
if self.backtesting.strategy.max_open_trades != float('inf') else -1} "max_open_trades": self.backtesting.strategy.max_open_trades
if self.backtesting.strategy.max_open_trades != float("inf")
else -1
}
return result return result
@@ -231,19 +243,19 @@ class Hyperopt:
""" """
result: Dict[str, Any] = {} result: Dict[str, Any] = {}
strategy = self.backtesting.strategy strategy = self.backtesting.strategy
if not HyperoptTools.has_space(self.config, 'roi'): if not HyperoptTools.has_space(self.config, "roi"):
result['roi'] = {str(k): v for k, v in strategy.minimal_roi.items()} result["roi"] = {str(k): v for k, v in strategy.minimal_roi.items()}
if not HyperoptTools.has_space(self.config, 'stoploss'): if not HyperoptTools.has_space(self.config, "stoploss"):
result['stoploss'] = {'stoploss': strategy.stoploss} result["stoploss"] = {"stoploss": strategy.stoploss}
if not HyperoptTools.has_space(self.config, 'trailing'): if not HyperoptTools.has_space(self.config, "trailing"):
result['trailing'] = { result["trailing"] = {
'trailing_stop': strategy.trailing_stop, "trailing_stop": strategy.trailing_stop,
'trailing_stop_positive': strategy.trailing_stop_positive, "trailing_stop_positive": strategy.trailing_stop_positive,
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset, "trailing_stop_positive_offset": strategy.trailing_stop_positive_offset,
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached, "trailing_only_offset_is_reached": strategy.trailing_only_offset_is_reached,
} }
if not HyperoptTools.has_space(self.config, 'trades'): if not HyperoptTools.has_space(self.config, "trades"):
result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades} result["max_open_trades"] = {"max_open_trades": strategy.max_open_trades}
return result return result
def print_results(self, results) -> None: def print_results(self, results) -> None:
@@ -251,14 +263,17 @@ class Hyperopt:
Log results if it is better than any previous evaluation Log results if it is better than any previous evaluation
TODO: this should be moved to HyperoptTools too TODO: this should be moved to HyperoptTools too
""" """
is_best = results['is_best'] is_best = results["is_best"]
if self.print_all or is_best: if self.print_all or is_best:
print( print(
HyperoptTools.get_result_table( HyperoptTools.get_result_table(
self.config, results, self.total_epochs, self.config,
self.print_all, self.print_colorized, results,
self.hyperopt_table_header self.total_epochs,
self.print_all,
self.print_colorized,
self.hyperopt_table_header,
) )
) )
self.hyperopt_table_header = 2 self.hyperopt_table_header = 2
@@ -267,41 +282,47 @@ class Hyperopt:
""" """
Assign the dimensions in the hyperoptimization space. Assign the dimensions in the hyperoptimization space.
""" """
if HyperoptTools.has_space(self.config, 'protection'): if HyperoptTools.has_space(self.config, "protection"):
# Protections can only be optimized when using the Parameter interface # Protections can only be optimized when using the Parameter interface
logger.debug("Hyperopt has 'protection' space") logger.debug("Hyperopt has 'protection' space")
# Enable Protections if protection space is selected. # Enable Protections if protection space is selected.
self.config['enable_protections'] = True self.config["enable_protections"] = True
self.backtesting.enable_protections = True self.backtesting.enable_protections = True
self.protection_space = self.custom_hyperopt.protection_space() self.protection_space = self.custom_hyperopt.protection_space()
if HyperoptTools.has_space(self.config, 'buy'): if HyperoptTools.has_space(self.config, "buy"):
logger.debug("Hyperopt has 'buy' space") logger.debug("Hyperopt has 'buy' space")
self.buy_space = self.custom_hyperopt.buy_indicator_space() self.buy_space = self.custom_hyperopt.buy_indicator_space()
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, "sell"):
logger.debug("Hyperopt has 'sell' space") logger.debug("Hyperopt has 'sell' space")
self.sell_space = self.custom_hyperopt.sell_indicator_space() self.sell_space = self.custom_hyperopt.sell_indicator_space()
if HyperoptTools.has_space(self.config, 'roi'): if HyperoptTools.has_space(self.config, "roi"):
logger.debug("Hyperopt has 'roi' space") logger.debug("Hyperopt has 'roi' space")
self.roi_space = self.custom_hyperopt.roi_space() self.roi_space = self.custom_hyperopt.roi_space()
if HyperoptTools.has_space(self.config, 'stoploss'): if HyperoptTools.has_space(self.config, "stoploss"):
logger.debug("Hyperopt has 'stoploss' space") logger.debug("Hyperopt has 'stoploss' space")
self.stoploss_space = self.custom_hyperopt.stoploss_space() self.stoploss_space = self.custom_hyperopt.stoploss_space()
if HyperoptTools.has_space(self.config, 'trailing'): if HyperoptTools.has_space(self.config, "trailing"):
logger.debug("Hyperopt has 'trailing' space") logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.trailing_space() self.trailing_space = self.custom_hyperopt.trailing_space()
if HyperoptTools.has_space(self.config, 'trades'): if HyperoptTools.has_space(self.config, "trades"):
logger.debug("Hyperopt has 'trades' space") logger.debug("Hyperopt has 'trades' space")
self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space() self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space()
self.dimensions = (self.buy_space + self.sell_space + self.protection_space self.dimensions = (
+ self.roi_space + self.stoploss_space + self.trailing_space self.buy_space
+ self.max_open_trades_space) + self.sell_space
+ self.protection_space
+ self.roi_space
+ self.stoploss_space
+ self.trailing_space
+ self.max_open_trades_space
)
def assign_params(self, params_dict: Dict, category: str) -> None: def assign_params(self, params_dict: Dict, category: str) -> None:
""" """
@@ -323,104 +344,119 @@ class Hyperopt:
params_dict = self._get_params_dict(self.dimensions, raw_params) params_dict = self._get_params_dict(self.dimensions, raw_params)
# Apply parameters # Apply parameters
if HyperoptTools.has_space(self.config, 'buy'): if HyperoptTools.has_space(self.config, "buy"):
self.assign_params(params_dict, 'buy') self.assign_params(params_dict, "buy")
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, "sell"):
self.assign_params(params_dict, 'sell') self.assign_params(params_dict, "sell")
if HyperoptTools.has_space(self.config, 'protection'): if HyperoptTools.has_space(self.config, "protection"):
self.assign_params(params_dict, 'protection') self.assign_params(params_dict, "protection")
if HyperoptTools.has_space(self.config, 'roi'): if HyperoptTools.has_space(self.config, "roi"):
self.backtesting.strategy.minimal_roi = ( self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(
self.custom_hyperopt.generate_roi_table(params_dict)) params_dict
)
if HyperoptTools.has_space(self.config, 'stoploss'): if HyperoptTools.has_space(self.config, "stoploss"):
self.backtesting.strategy.stoploss = params_dict['stoploss'] self.backtesting.strategy.stoploss = params_dict["stoploss"]
if HyperoptTools.has_space(self.config, 'trailing'): if HyperoptTools.has_space(self.config, "trailing"):
d = self.custom_hyperopt.generate_trailing_params(params_dict) d = self.custom_hyperopt.generate_trailing_params(params_dict)
self.backtesting.strategy.trailing_stop = d['trailing_stop'] self.backtesting.strategy.trailing_stop = d["trailing_stop"]
self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive'] self.backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"]
self.backtesting.strategy.trailing_stop_positive_offset = \ self.backtesting.strategy.trailing_stop_positive_offset = d[
d['trailing_stop_positive_offset'] "trailing_stop_positive_offset"
self.backtesting.strategy.trailing_only_offset_is_reached = \ ]
d['trailing_only_offset_is_reached'] self.backtesting.strategy.trailing_only_offset_is_reached = d[
"trailing_only_offset_is_reached"
]
if HyperoptTools.has_space(self.config, 'trades'): if HyperoptTools.has_space(self.config, "trades"):
if self.config["stake_amount"] == "unlimited" and \ if self.config["stake_amount"] == "unlimited" and (
(params_dict['max_open_trades'] == -1 or params_dict['max_open_trades'] == 0): params_dict["max_open_trades"] == -1 or params_dict["max_open_trades"] == 0
):
# Ignore unlimited max open trades if stake amount is unlimited # Ignore unlimited max open trades if stake amount is unlimited
params_dict.update({'max_open_trades': self.config['max_open_trades']}) params_dict.update({"max_open_trades": self.config["max_open_trades"]})
updated_max_open_trades = int(params_dict['max_open_trades']) \ updated_max_open_trades = (
if (params_dict['max_open_trades'] != -1 int(params_dict["max_open_trades"])
and params_dict['max_open_trades'] != 0) else float('inf') if (params_dict["max_open_trades"] != -1 and params_dict["max_open_trades"] != 0)
else float("inf")
)
self.config.update({'max_open_trades': updated_max_open_trades}) self.config.update({"max_open_trades": updated_max_open_trades})
self.backtesting.strategy.max_open_trades = updated_max_open_trades self.backtesting.strategy.max_open_trades = updated_max_open_trades
with self.data_pickle_file.open('rb') as f: with self.data_pickle_file.open("rb") as f:
processed = load(f, mmap_mode='r') processed = load(f, mmap_mode="r")
if self.analyze_per_epoch: if self.analyze_per_epoch:
# Data is not yet analyzed, rerun populate_indicators. # Data is not yet analyzed, rerun populate_indicators.
processed = self.advise_and_trim(processed) processed = self.advise_and_trim(processed)
bt_results = self.backtesting.backtest( bt_results = self.backtesting.backtest(
processed=processed, processed=processed, start_date=self.min_date, end_date=self.max_date
start_date=self.min_date,
end_date=self.max_date
) )
backtest_end_time = datetime.now(timezone.utc) backtest_end_time = datetime.now(timezone.utc)
bt_results.update({ bt_results.update(
'backtest_start_time': int(backtest_start_time.timestamp()), {
'backtest_end_time': int(backtest_end_time.timestamp()), "backtest_start_time": int(backtest_start_time.timestamp()),
}) "backtest_end_time": int(backtest_end_time.timestamp()),
}
)
return self._get_results_dict(bt_results, self.min_date, self.max_date, return self._get_results_dict(
params_dict, bt_results, self.min_date, self.max_date, params_dict, processed=processed
processed=processed) )
def _get_results_dict(self, backtesting_results, min_date, max_date, def _get_results_dict(
params_dict, processed: Dict[str, DataFrame] self, backtesting_results, min_date, max_date, params_dict, processed: Dict[str, DataFrame]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
params_details = self._get_params_details(params_dict) params_details = self._get_params_details(params_dict)
strat_stats = generate_strategy_stats( strat_stats = generate_strategy_stats(
self.pairlist, self.backtesting.strategy.get_strategy_name(), self.pairlist,
backtesting_results, min_date, max_date, market_change=self.market_change, self.backtesting.strategy.get_strategy_name(),
backtesting_results,
min_date,
max_date,
market_change=self.market_change,
is_hyperopt=True, is_hyperopt=True,
) )
results_explanation = HyperoptTools.format_results_explanation_string( results_explanation = HyperoptTools.format_results_explanation_string(
strat_stats, self.config['stake_currency']) strat_stats, self.config["stake_currency"]
)
not_optimized = self.backtesting.strategy.get_no_optimize_params() not_optimized = self.backtesting.strategy.get_no_optimize_params()
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details()) not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
trade_count = strat_stats['total_trades'] trade_count = strat_stats["total_trades"]
total_profit = strat_stats['profit_total'] total_profit = strat_stats["profit_total"]
# If this evaluation contains too short amount of trades to be # If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value) # interesting -- consider it as 'bad' (assigned max. loss value)
# in order to cast this hyperspace point away from optimization # in order to cast this hyperspace point away from optimization
# path. We do not want to optimize 'hodl' strategies. # path. We do not want to optimize 'hodl' strategies.
loss: float = MAX_LOSS loss: float = MAX_LOSS
if trade_count >= self.config['hyperopt_min_trades']: if trade_count >= self.config["hyperopt_min_trades"]:
loss = self.calculate_loss(results=backtesting_results['results'], loss = self.calculate_loss(
trade_count=trade_count, results=backtesting_results["results"],
min_date=min_date, max_date=max_date, trade_count=trade_count,
config=self.config, processed=processed, min_date=min_date,
backtest_stats=strat_stats) max_date=max_date,
config=self.config,
processed=processed,
backtest_stats=strat_stats,
)
return { return {
'loss': loss, "loss": loss,
'params_dict': params_dict, "params_dict": params_dict,
'params_details': params_details, "params_details": params_details,
'params_not_optimized': not_optimized, "params_not_optimized": not_optimized,
'results_metrics': strat_stats, "results_metrics": strat_stats,
'results_explanation': results_explanation, "results_explanation": results_explanation,
'total_profit': total_profit, "total_profit": total_profit,
} }
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
@@ -439,16 +475,16 @@ class Hyperopt:
base_estimator=estimator, base_estimator=estimator,
acq_optimizer=acq_optimizer, acq_optimizer=acq_optimizer,
n_initial_points=INITIAL_POINTS, n_initial_points=INITIAL_POINTS,
acq_optimizer_kwargs={'n_jobs': cpu_count}, acq_optimizer_kwargs={"n_jobs": cpu_count},
random_state=self.random_state, random_state=self.random_state,
model_queue_size=SKOPT_MODEL_QUEUE_SIZE, model_queue_size=SKOPT_MODEL_QUEUE_SIZE,
) )
def run_optimizer_parallel( def run_optimizer_parallel(self, parallel: Parallel, asked: List[List]) -> List[Dict[str, Any]]:
self, parallel: Parallel, asked: List[List]) -> List[Dict[str, Any]]: """Start optimizer in a parallel way"""
""" Start optimizer in a parallel way """ return parallel(
return parallel(delayed( delayed(wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked
wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked) )
def _set_random_state(self, random_state: Optional[int]) -> int: def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1) return random_state or random.randint(1, 2**16 - 1)
@@ -462,7 +498,7 @@ class Hyperopt:
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
self.min_date, self.max_date = get_timerange(trimmed) self.min_date, self.max_date = get_timerange(trimmed)
if not self.market_change: if not self.market_change:
self.market_change = calculate_market_change(trimmed, 'close') self.market_change = calculate_market_change(trimmed, "close")
# Real trimming will happen as part of backtesting. # Real trimming will happen as part of backtesting.
return preprocessed return preprocessed
@@ -478,10 +514,12 @@ class Hyperopt:
preprocessed = self.advise_and_trim(data) preprocessed = self.advise_and_trim(data)
logger.info(f'Hyperopting with data from ' logger.info(
f'{self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' f"Hyperopting with data from "
f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' f"{self.min_date.strftime(DATETIME_PRINT_FORMAT)} "
f'({(self.max_date - self.min_date).days} days)..') f"up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} "
f"({(self.max_date - self.min_date).days} days).."
)
# Store non-trimmed data - will be trimmed after signal generation. # Store non-trimmed data - will be trimmed after signal generation.
dump(preprocessed, self.data_pickle_file) dump(preprocessed, self.data_pickle_file)
else: else:
@@ -499,12 +537,14 @@ class Hyperopt:
5. Repeat until at least `n_points` points in the `asked_non_tried` list 5. Repeat until at least `n_points` points in the `asked_non_tried` list
6. Return a list with length truncated at `n_points` 6. Return a list with length truncated at `n_points`
""" """
def unique_list(a_list): def unique_list(a_list):
new_list = [] new_list = []
for item in a_list: for item in a_list:
if item not in new_list: if item not in new_list:
new_list.append(item) new_list.append(item)
return new_list return new_list
i = 0 i = 0
asked_non_tried: List[List[Any]] = [] asked_non_tried: List[List[Any]] = []
is_random_non_tried: List[bool] = [] is_random_non_tried: List[bool] = []
@@ -516,18 +556,20 @@ class Hyperopt:
else: else:
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
is_random = [True for _ in range(len(asked))] is_random = [True for _ in range(len(asked))]
is_random_non_tried += [rand for x, rand in zip(asked, is_random) is_random_non_tried += [
if x not in self.opt.Xi rand
and x not in asked_non_tried] for x, rand in zip(asked, is_random)
asked_non_tried += [x for x in asked if x not in self.opt.Xi and x not in asked_non_tried
if x not in self.opt.Xi ]
and x not in asked_non_tried] asked_non_tried += [
x for x in asked if x not in self.opt.Xi and x not in asked_non_tried
]
i += 1 i += 1
if asked_non_tried: if asked_non_tried:
return ( return (
asked_non_tried[:min(len(asked_non_tried), n_points)], asked_non_tried[: min(len(asked_non_tried), n_points)],
is_random_non_tried[:min(len(asked_non_tried), n_points)] is_random_non_tried[: min(len(asked_non_tried), n_points)],
) )
else: else:
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
@@ -536,8 +578,8 @@ class Hyperopt:
""" """
Evaluate results returned from generate_optimizer Evaluate results returned from generate_optimizer
""" """
val['current_epoch'] = current val["current_epoch"] = current
val['is_initial_point'] = current <= INITIAL_POINTS val["is_initial_point"] = current <= INITIAL_POINTS
logger.debug("Optimizer epoch evaluated: %s", val) logger.debug("Optimizer epoch evaluated: %s", val)
@@ -546,18 +588,18 @@ class Hyperopt:
# to keep proper order in the list of results. That's because # to keep proper order in the list of results. That's because
# evaluations can take different time. Here they are aligned in the # evaluations can take different time. Here they are aligned in the
# order they will be shown to the user. # order they will be shown to the user.
val['is_best'] = is_best val["is_best"] = is_best
val['is_random'] = is_random val["is_random"] = is_random
self.print_results(val) self.print_results(val)
if is_best: if is_best:
self.current_best_loss = val['loss'] self.current_best_loss = val["loss"]
self.current_best_epoch = val self.current_best_epoch = val
self._save_result(val) self._save_result(val)
def start(self) -> None: def start(self) -> None:
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state')) self.random_state = self._set_random_state(self.config.get("hyperopt_random_state"))
logger.info(f"Using optimizer random state: {self.random_state}") logger.info(f"Using optimizer random state: {self.random_state}")
self.hyperopt_table_header = -1 self.hyperopt_table_header = -1
# Initialize spaces ... # Initialize spaces ...
@@ -577,8 +619,8 @@ class Hyperopt:
cpus = cpu_count() cpus = cpu_count()
logger.info(f"Found {cpus} CPU cores. Let's make them scream!") logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
config_jobs = self.config.get('hyperopt_jobs', -1) config_jobs = self.config.get("hyperopt_jobs", -1)
logger.info(f'Number of parallel jobs set as: {config_jobs}') logger.info(f"Number of parallel jobs set as: {config_jobs}")
self.opt = self.get_optimizer(self.dimensions, config_jobs) self.opt = self.get_optimizer(self.dimensions, config_jobs)
@@ -588,7 +630,7 @@ class Hyperopt:
try: try:
with Parallel(n_jobs=config_jobs) as parallel: with Parallel(n_jobs=config_jobs) as parallel:
jobs = parallel._effective_n_jobs() jobs = parallel._effective_n_jobs()
logger.info(f'Effective number of parallel workers used: {jobs}') logger.info(f"Effective number of parallel workers used: {jobs}")
# Define progressbar # Define progressbar
with Progress( with Progress(
@@ -611,7 +653,7 @@ class Hyperopt:
# This allows dataprovider to load it's informative cache. # This allows dataprovider to load it's informative cache.
asked, is_random = self.get_asked_points(n_points=1) asked, is_random = self.get_asked_points(n_points=1)
f_val0 = self.generate_optimizer(asked[0]) f_val0 = self.generate_optimizer(asked[0])
self.opt.tell(asked, [f_val0['loss']]) self.opt.tell(asked, [f_val0["loss"]])
self.evaluate_result(f_val0, 1, is_random[0]) self.evaluate_result(f_val0, 1, is_random[0])
pbar.update(task, advance=1) pbar.update(task, advance=1)
start += 1 start += 1
@@ -625,7 +667,7 @@ class Hyperopt:
asked, is_random = self.get_asked_points(n_points=current_jobs) asked, is_random = self.get_asked_points(n_points=current_jobs)
f_val = self.run_optimizer_parallel(parallel, asked) f_val = self.run_optimizer_parallel(parallel, asked)
self.opt.tell(asked, [v['loss'] for v in f_val]) self.opt.tell(asked, [v["loss"] for v in f_val])
for j, val in enumerate(f_val): for j, val in enumerate(f_val):
# Use human-friendly indexes here (starting from 1) # Use human-friendly indexes here (starting from 1)
@@ -635,23 +677,26 @@ class Hyperopt:
pbar.update(task, advance=1) pbar.update(task, advance=1)
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print("User interrupted..")
logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " logger.info(
f"saved to '{self.results_file}'.") f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'."
)
if self.current_best_epoch: if self.current_best_epoch:
HyperoptTools.try_export_params( HyperoptTools.try_export_params(
self.config, self.config, self.backtesting.strategy.get_strategy_name(), self.current_best_epoch
self.backtesting.strategy.get_strategy_name(), )
self.current_best_epoch)
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs, HyperoptTools.show_epoch_details(
self.print_json) self.current_best_epoch, self.total_epochs, self.print_json
)
elif self.num_epochs_saved > 0: elif self.num_epochs_saved > 0:
print( print(
f"No good result found for given optimization function in {self.num_epochs_saved} " f"No good result found for given optimization function in {self.num_epochs_saved} "
f"{plural(self.num_epochs_saved, 'epoch')}.") f"{plural(self.num_epochs_saved, 'epoch')}."
)
else: else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have # This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated. # a chance to be evaluated.
+28 -25
View File
@@ -3,6 +3,7 @@ HyperOptAuto class.
This module implements a convenience auto-hyperopt class, which can be used together with strategies This module implements a convenience auto-hyperopt class, which can be used together with strategies
that implement IHyperStrategy interface. that implement IHyperStrategy interface.
""" """
import logging import logging
from contextlib import suppress from contextlib import suppress
from typing import Callable, Dict, List from typing import Callable, Dict, List
@@ -20,15 +21,17 @@ logger = logging.getLogger(__name__)
def _format_exception_message(space: str, ignore_missing_space: bool) -> None: def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
msg = (f"The '{space}' space is included into the hyperoptimization " msg = (
f"but no parameter for this space was found in your Strategy. " f"The '{space}' space is included into the hyperoptimization "
) f"but no parameter for this space was found in your Strategy. "
)
if ignore_missing_space: if ignore_missing_space:
logger.warning(msg + "This space will be ignored.") logger.warning(msg + "This space will be ignored.")
else: else:
raise OperationalException( raise OperationalException(
msg + f"Please make sure to have parameters for this space enabled for optimization " msg + f"Please make sure to have parameters for this space enabled for optimization "
f"or remove the '{space}' space from hyperoptimization.") f"or remove the '{space}' space from hyperoptimization."
)
class HyperOptAuto(IHyperOpt): class HyperOptAuto(IHyperOpt):
@@ -44,7 +47,7 @@ class HyperOptAuto(IHyperOpt):
:param name: function name. :param name: function name.
:return: a requested function. :return: a requested function.
""" """
hyperopt_cls = getattr(self.strategy, 'HyperOpt', None) hyperopt_cls = getattr(self.strategy, "HyperOpt", None)
default_func = getattr(super(), name) default_func = getattr(super(), name)
if hyperopt_cls: if hyperopt_cls:
return getattr(hyperopt_cls, name, default_func) return getattr(hyperopt_cls, name, default_func)
@@ -63,36 +66,36 @@ class HyperOptAuto(IHyperOpt):
return indicator_space return indicator_space
else: else:
_format_exception_message( _format_exception_message(
category, category, self.config.get("hyperopt_ignore_missing_space", False)
self.config.get("hyperopt_ignore_missing_space", False)) )
return [] return []
def buy_indicator_space(self) -> List['Dimension']: def buy_indicator_space(self) -> List["Dimension"]:
return self._get_indicator_space('buy') return self._get_indicator_space("buy")
def sell_indicator_space(self) -> List['Dimension']: def sell_indicator_space(self) -> List["Dimension"]:
return self._get_indicator_space('sell') return self._get_indicator_space("sell")
def protection_space(self) -> List['Dimension']: def protection_space(self) -> List["Dimension"]:
return self._get_indicator_space('protection') return self._get_indicator_space("protection")
def generate_roi_table(self, params: Dict) -> Dict[int, float]: def generate_roi_table(self, params: Dict) -> Dict[int, float]:
return self._get_func('generate_roi_table')(params) return self._get_func("generate_roi_table")(params)
def roi_space(self) -> List['Dimension']: def roi_space(self) -> List["Dimension"]:
return self._get_func('roi_space')() return self._get_func("roi_space")()
def stoploss_space(self) -> List['Dimension']: def stoploss_space(self) -> List["Dimension"]:
return self._get_func('stoploss_space')() return self._get_func("stoploss_space")()
def generate_trailing_params(self, params: Dict) -> Dict: def generate_trailing_params(self, params: Dict) -> Dict:
return self._get_func('generate_trailing_params')(params) return self._get_func("generate_trailing_params")(params)
def trailing_space(self) -> List['Dimension']: def trailing_space(self) -> List["Dimension"]:
return self._get_func('trailing_space')() return self._get_func("trailing_space")()
def max_open_trades_space(self) -> List['Dimension']: def max_open_trades_space(self) -> List["Dimension"]:
return self._get_func('max_open_trades_space')() return self._get_func("max_open_trades_space")()
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType: def generate_estimator(self, dimensions: List["Dimension"], **kwargs) -> EstimatorType:
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs) return self._get_func("generate_estimator")(dimensions=dimensions, **kwargs)
+49 -54
View File
@@ -11,11 +11,10 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True)
""" """
Filter our items from the list of hyperopt results Filter our items from the list of hyperopt results
""" """
if filteroptions['only_best']: if filteroptions["only_best"]:
epochs = [x for x in epochs if x['is_best']] epochs = [x for x in epochs if x["is_best"]]
if filteroptions['only_profitable']: if filteroptions["only_profitable"]:
epochs = [x for x in epochs epochs = [x for x in epochs if x["results_metrics"].get("profit_total", 0) > 0]
if x['results_metrics'].get('profit_total', 0) > 0]
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
@@ -25,10 +24,12 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True)
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions) epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
if log: if log:
logger.info(f"{len(epochs)} " + logger.info(
("best " if filteroptions['only_best'] else "") + f"{len(epochs)} "
("profitable " if filteroptions['only_profitable'] else "") + + ("best " if filteroptions["only_best"] else "")
"epochs found.") + ("profitable " if filteroptions["only_profitable"] else "")
+ "epochs found."
)
return epochs return epochs
@@ -36,93 +37,87 @@ def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
""" """
Filter epochs with trade-counts > trades Filter epochs with trade-counts > trades
""" """
return [ return [x for x in epochs if x["results_metrics"].get("total_trades", 0) > trade_count]
x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count
]
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
if filteroptions["filter_min_trades"] > 0:
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions["filter_min_trades"])
if filteroptions['filter_min_trades'] > 0: if filteroptions["filter_max_trades"] > 0:
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
if filteroptions['filter_max_trades'] > 0:
epochs = [ epochs = [
x for x in epochs x
if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades'] for x in epochs
if x["results_metrics"].get("total_trades") < filteroptions["filter_max_trades"]
] ]
return epochs return epochs
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def get_duration_value(x): def get_duration_value(x):
# Duration in minutes ... # Duration in minutes ...
if 'holding_avg_s' in x['results_metrics']: if "holding_avg_s" in x["results_metrics"]:
avg = x['results_metrics']['holding_avg_s'] avg = x["results_metrics"]["holding_avg_s"]
return avg // 60 return avg // 60
raise OperationalException( raise OperationalException(
"Holding-average not available. Please omit the filter on average time, " "Holding-average not available. Please omit the filter on average time, "
"or rerun hyperopt with this version") "or rerun hyperopt with this version"
)
if filteroptions['filter_min_avg_time'] is not None: if filteroptions["filter_min_avg_time"] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [ epochs = [x for x in epochs if get_duration_value(x) > filteroptions["filter_min_avg_time"]]
x for x in epochs if filteroptions["filter_max_avg_time"] is not None:
if get_duration_value(x) > filteroptions['filter_min_avg_time']
]
if filteroptions['filter_max_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [ epochs = [x for x in epochs if get_duration_value(x) < filteroptions["filter_max_avg_time"]]
x for x in epochs
if get_duration_value(x) < filteroptions['filter_max_avg_time']
]
return epochs return epochs
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
if filteroptions["filter_min_avg_profit"] is not None:
if filteroptions['filter_min_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [ epochs = [
x for x in epochs x
if x['results_metrics'].get('profit_mean', 0) * 100 for x in epochs
> filteroptions['filter_min_avg_profit'] if x["results_metrics"].get("profit_mean", 0) * 100
> filteroptions["filter_min_avg_profit"]
] ]
if filteroptions['filter_max_avg_profit'] is not None: if filteroptions["filter_max_avg_profit"] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [ epochs = [
x for x in epochs x
if x['results_metrics'].get('profit_mean', 0) * 100 for x in epochs
< filteroptions['filter_max_avg_profit'] if x["results_metrics"].get("profit_mean", 0) * 100
< filteroptions["filter_max_avg_profit"]
] ]
if filteroptions['filter_min_total_profit'] is not None: if filteroptions["filter_min_total_profit"] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [ epochs = [
x for x in epochs x
if x['results_metrics'].get('profit_total_abs', 0) for x in epochs
> filteroptions['filter_min_total_profit'] if x["results_metrics"].get("profit_total_abs", 0)
> filteroptions["filter_min_total_profit"]
] ]
if filteroptions['filter_max_total_profit'] is not None: if filteroptions["filter_max_total_profit"] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [ epochs = [
x for x in epochs x
if x['results_metrics'].get('profit_total_abs', 0) for x in epochs
< filteroptions['filter_max_total_profit'] if x["results_metrics"].get("profit_total_abs", 0)
< filteroptions["filter_max_total_profit"]
] ]
return epochs return epochs
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
if filteroptions["filter_min_objective"] is not None:
if filteroptions['filter_min_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] epochs = [x for x in epochs if x["loss"] < filteroptions["filter_min_objective"]]
if filteroptions['filter_max_objective'] is not None: if filteroptions["filter_max_objective"] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0) epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] epochs = [x for x in epochs if x["loss"] > filteroptions["filter_max_objective"]]
return epochs return epochs
+58 -55
View File
@@ -2,6 +2,7 @@
IHyperOpt interface IHyperOpt interface
This module defines the interface to apply for hyperopt This module defines the interface to apply for hyperopt
""" """
import logging import logging
import math import math
from abc import ABC from abc import ABC
@@ -30,6 +31,7 @@ class IHyperOpt(ABC):
Class attributes you can use: Class attributes you can use:
timeframe -> int: value of the timeframe to use for the strategy timeframe -> int: value of the timeframe to use for the strategy
""" """
timeframe: str timeframe: str
strategy: IStrategy strategy: IStrategy
@@ -37,7 +39,7 @@ class IHyperOpt(ABC):
self.config = config self.config = config
# Assign timeframe to be used in hyperopt # Assign timeframe to be used in hyperopt
IHyperOpt.timeframe = str(config['timeframe']) IHyperOpt.timeframe = str(config["timeframe"])
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType: def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
""" """
@@ -45,7 +47,7 @@ class IHyperOpt(ABC):
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
inheriting from RegressorMixin (from sklearn). inheriting from RegressorMixin (from sklearn).
""" """
return 'ET' return "ET"
def generate_roi_table(self, params: Dict) -> Dict[int, float]: def generate_roi_table(self, params: Dict) -> Dict[int, float]:
""" """
@@ -55,10 +57,10 @@ class IHyperOpt(ABC):
You may override it in your custom Hyperopt class. You may override it in your custom Hyperopt class.
""" """
roi_table = {} roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] roi_table[0] = params["roi_p1"] + params["roi_p2"] + params["roi_p3"]
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] roi_table[params["roi_t3"]] = params["roi_p1"] + params["roi_p2"]
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] roi_table[params["roi_t3"] + params["roi_t2"]] = params["roi_p1"]
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 roi_table[params["roi_t3"] + params["roi_t2"] + params["roi_t1"]] = 0
return roi_table return roi_table
@@ -96,49 +98,52 @@ class IHyperOpt(ABC):
roi_t_scale = timeframe_min / 5 roi_t_scale = timeframe_min / 5
roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_p_scale = math.log1p(timeframe_min) / math.log1p(5)
roi_limits = { roi_limits = {
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), "roi_t1_min": int(10 * roi_t_scale * roi_t_alpha),
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), "roi_t1_max": int(120 * roi_t_scale * roi_t_alpha),
'roi_t2_min': int(10 * roi_t_scale * roi_t_alpha), "roi_t2_min": int(10 * roi_t_scale * roi_t_alpha),
'roi_t2_max': int(60 * roi_t_scale * roi_t_alpha), "roi_t2_max": int(60 * roi_t_scale * roi_t_alpha),
'roi_t3_min': int(10 * roi_t_scale * roi_t_alpha), "roi_t3_min": int(10 * roi_t_scale * roi_t_alpha),
'roi_t3_max': int(40 * roi_t_scale * roi_t_alpha), "roi_t3_max": int(40 * roi_t_scale * roi_t_alpha),
'roi_p1_min': 0.01 * roi_p_scale * roi_p_alpha, "roi_p1_min": 0.01 * roi_p_scale * roi_p_alpha,
'roi_p1_max': 0.04 * roi_p_scale * roi_p_alpha, "roi_p1_max": 0.04 * roi_p_scale * roi_p_alpha,
'roi_p2_min': 0.01 * roi_p_scale * roi_p_alpha, "roi_p2_min": 0.01 * roi_p_scale * roi_p_alpha,
'roi_p2_max': 0.07 * roi_p_scale * roi_p_alpha, "roi_p2_max": 0.07 * roi_p_scale * roi_p_alpha,
'roi_p3_min': 0.01 * roi_p_scale * roi_p_alpha, "roi_p3_min": 0.01 * roi_p_scale * roi_p_alpha,
'roi_p3_max': 0.20 * roi_p_scale * roi_p_alpha, "roi_p3_max": 0.20 * roi_p_scale * roi_p_alpha,
} }
logger.debug(f"Using roi space limits: {roi_limits}") logger.debug(f"Using roi space limits: {roi_limits}")
p = { p = {
'roi_t1': roi_limits['roi_t1_min'], "roi_t1": roi_limits["roi_t1_min"],
'roi_t2': roi_limits['roi_t2_min'], "roi_t2": roi_limits["roi_t2_min"],
'roi_t3': roi_limits['roi_t3_min'], "roi_t3": roi_limits["roi_t3_min"],
'roi_p1': roi_limits['roi_p1_min'], "roi_p1": roi_limits["roi_p1_min"],
'roi_p2': roi_limits['roi_p2_min'], "roi_p2": roi_limits["roi_p2_min"],
'roi_p3': roi_limits['roi_p3_min'], "roi_p3": roi_limits["roi_p3_min"],
} }
logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}") logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}")
p = { p = {
'roi_t1': roi_limits['roi_t1_max'], "roi_t1": roi_limits["roi_t1_max"],
'roi_t2': roi_limits['roi_t2_max'], "roi_t2": roi_limits["roi_t2_max"],
'roi_t3': roi_limits['roi_t3_max'], "roi_t3": roi_limits["roi_t3_max"],
'roi_p1': roi_limits['roi_p1_max'], "roi_p1": roi_limits["roi_p1_max"],
'roi_p2': roi_limits['roi_p2_max'], "roi_p2": roi_limits["roi_p2_max"],
'roi_p3': roi_limits['roi_p3_max'], "roi_p3": roi_limits["roi_p3_max"],
} }
logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}")
return [ return [
Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], name="roi_t1"),
Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], name="roi_t2"),
Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), Integer(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], name="roi_t3"),
SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=3, SKDecimal(
name='roi_p1'), roi_limits["roi_p1_min"], roi_limits["roi_p1_max"], decimals=3, name="roi_p1"
SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=3, ),
name='roi_p2'), SKDecimal(
SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=3, roi_limits["roi_p2_min"], roi_limits["roi_p2_max"], decimals=3, name="roi_p2"
name='roi_p3'), ),
SKDecimal(
roi_limits["roi_p3_min"], roi_limits["roi_p3_max"], decimals=3, name="roi_p3"
),
] ]
def stoploss_space(self) -> List[Dimension]: def stoploss_space(self) -> List[Dimension]:
@@ -149,7 +154,7 @@ class IHyperOpt(ABC):
You may override it in your custom Hyperopt class. You may override it in your custom Hyperopt class.
""" """
return [ return [
SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), SKDecimal(-0.35, -0.02, decimals=3, name="stoploss"),
] ]
def generate_trailing_params(self, params: Dict) -> Dict: def generate_trailing_params(self, params: Dict) -> Dict:
@@ -157,11 +162,12 @@ class IHyperOpt(ABC):
Create dict with trailing stop parameters. Create dict with trailing stop parameters.
""" """
return { return {
'trailing_stop': params['trailing_stop'], "trailing_stop": params["trailing_stop"],
'trailing_stop_positive': params['trailing_stop_positive'], "trailing_stop_positive": params["trailing_stop_positive"],
'trailing_stop_positive_offset': (params['trailing_stop_positive'] + "trailing_stop_positive_offset": (
params['trailing_stop_positive_offset_p1']), params["trailing_stop_positive"] + params["trailing_stop_positive_offset_p1"]
'trailing_only_offset_is_reached': params['trailing_only_offset_is_reached'], ),
"trailing_only_offset_is_reached": params["trailing_only_offset_is_reached"],
} }
def trailing_space(self) -> List[Dimension]: def trailing_space(self) -> List[Dimension]:
@@ -177,18 +183,15 @@ class IHyperOpt(ABC):
# This parameter is included into the hyperspace dimensions rather than assigning # This parameter is included into the hyperspace dimensions rather than assigning
# it explicitly in the code in order to have it printed in the results along with # it explicitly in the code in order to have it printed in the results along with
# other 'trailing' hyperspace parameters. # other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'), Categorical([True], name="trailing_stop"),
SKDecimal(0.01, 0.35, decimals=3, name="trailing_stop_positive"),
SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'),
# 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
# so this intermediate parameter is used as the value of the difference between # so this intermediate parameter is used as the value of the difference between
# them. The value of the 'trailing_stop_positive_offset' is constructed in the # them. The value of the 'trailing_stop_positive_offset' is constructed in the
# generate_trailing_params() method. # generate_trailing_params() method.
# This is similar to the hyperspace dimensions used for constructing the ROI tables. # This is similar to the hyperspace dimensions used for constructing the ROI tables.
SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), SKDecimal(0.001, 0.1, decimals=3, name="trailing_stop_positive_offset_p1"),
Categorical([True, False], name="trailing_only_offset_is_reached"),
Categorical([True, False], name='trailing_only_offset_is_reached'),
] ]
def max_open_trades_space(self) -> List[Dimension]: def max_open_trades_space(self) -> List[Dimension]:
@@ -198,7 +201,7 @@ class IHyperOpt(ABC):
You may override it in your custom Hyperopt class. You may override it in your custom Hyperopt class.
""" """
return [ return [
Integer(-1, 10, name='max_open_trades'), Integer(-1, 10, name="max_open_trades"),
] ]
# This is needed for proper unpickling the class attribute timeframe # This is needed for proper unpickling the class attribute timeframe
@@ -206,9 +209,9 @@ class IHyperOpt(ABC):
# Why do I still need such shamanic mantras in modern python? # Why do I still need such shamanic mantras in modern python?
def __getstate__(self): def __getstate__(self):
state = self.__dict__.copy() state = self.__dict__.copy()
state['timeframe'] = self.timeframe state["timeframe"] = self.timeframe
return state return state
def __setstate__(self, state): def __setstate__(self, state):
self.__dict__.update(state) self.__dict__.update(state)
IHyperOpt.timeframe = state['timeframe'] IHyperOpt.timeframe = state["timeframe"]
+12 -5
View File
@@ -17,15 +17,22 @@ class IHyperOptLoss(ABC):
Interface for freqtrade hyperopt Loss functions. Interface for freqtrade hyperopt Loss functions.
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
""" """
timeframe: str timeframe: str
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def hyperopt_loss_function(*, results: DataFrame, trade_count: int, def hyperopt_loss_function(
min_date: datetime, max_date: datetime, *,
config: Config, processed: Dict[str, DataFrame], results: DataFrame,
backtest_stats: Dict[str, Any], trade_count: int,
**kwargs) -> float: min_date: datetime,
max_date: datetime,
config: Config,
processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
**kwargs,
) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results
""" """
+265 -184
View File
@@ -37,7 +37,8 @@ def hyperopt_serializer(x):
class HyperoptStateContainer: class HyperoptStateContainer:
""" Singleton class to track state of hyperopt""" """Singleton class to track state of hyperopt"""
state: HyperoptState = HyperoptState.OPTIMIZE state: HyperoptState = HyperoptState.OPTIMIZE
@classmethod @classmethod
@@ -46,20 +47,21 @@ class HyperoptStateContainer:
class HyperoptTools: class HyperoptTools:
@staticmethod @staticmethod
def get_strategy_filename(config: Config, strategy_name: str) -> Optional[Path]: def get_strategy_filename(config: Config, strategy_name: str) -> Optional[Path]:
""" """
Get Strategy-location (filename) from strategy_name Get Strategy-location (filename) from strategy_name
""" """
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy_objs = StrategyResolver.search_all_objects( strategy_objs = StrategyResolver.search_all_objects(
config, False, config.get('recursive_strategy_search', False)) config, False, config.get("recursive_strategy_search", False)
strategies = [s for s in strategy_objs if s['name'] == strategy_name] )
strategies = [s for s in strategy_objs if s["name"] == strategy_name]
if strategies: if strategies:
strategy = strategies[0] strategy = strategies[0]
return Path(strategy['location']) return Path(strategy["location"])
return None return None
@staticmethod @staticmethod
@@ -67,37 +69,40 @@ class HyperoptTools:
""" """
Generate files Generate files
""" """
final_params = deepcopy(params['params_not_optimized']) final_params = deepcopy(params["params_not_optimized"])
final_params = deep_merge_dicts(params['params_details'], final_params) final_params = deep_merge_dicts(params["params_details"], final_params)
final_params = { final_params = {
'strategy_name': strategy_name, "strategy_name": strategy_name,
'params': final_params, "params": final_params,
'ft_stratparam_v': 1, "ft_stratparam_v": 1,
'export_time': datetime.now(timezone.utc), "export_time": datetime.now(timezone.utc),
} }
logger.info(f"Dumping parameters to {filename}") logger.info(f"Dumping parameters to {filename}")
with filename.open('w') as f: with filename.open("w") as f:
rapidjson.dump(final_params, f, indent=2, rapidjson.dump(
default=hyperopt_serializer, final_params,
number_mode=HYPER_PARAMS_FILE_FORMAT f,
) indent=2,
default=hyperopt_serializer,
number_mode=HYPER_PARAMS_FILE_FORMAT,
)
@staticmethod @staticmethod
def load_params(filename: Path) -> Dict: def load_params(filename: Path) -> Dict:
""" """
Load parameters from file Load parameters from file
""" """
with filename.open('r') as f: with filename.open("r") as f:
params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT) params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT)
return params return params
@staticmethod @staticmethod
def try_export_params(config: Config, strategy_name: str, params: Dict): def try_export_params(config: Config, strategy_name: str, params: Dict):
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get("disableparamexport", False):
# Export parameters ... # Export parameters ...
fn = HyperoptTools.get_strategy_filename(config, strategy_name) fn = HyperoptTools.get_strategy_filename(config, strategy_name)
if fn: if fn:
HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json')) HyperoptTools.export_params(params, strategy_name, fn.with_suffix(".json"))
else: else:
logger.warning("Strategy not found, not exporting parameter file.") logger.warning("Strategy not found, not exporting parameter file.")
@@ -107,10 +112,10 @@ class HyperoptTools:
Tell if the space value is contained in the configuration Tell if the space value is contained in the configuration
""" """
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces # 'trailing' and 'protection spaces are not included in the 'default' set of spaces
if space in ('trailing', 'protection', 'trades'): if space in ("trailing", "protection", "trades"):
return any(s in config['spaces'] for s in [space, 'all']) return any(s in config["spaces"] for s in [space, "all"])
else: else:
return any(s in config['spaces'] for s in [space, 'all', 'default']) return any(s in config["spaces"] for s in [space, "all", "default"])
@staticmethod @staticmethod
def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]: def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
@@ -118,8 +123,9 @@ class HyperoptTools:
Stream hyperopt results from file Stream hyperopt results from file
""" """
import rapidjson import rapidjson
logger.info(f"Reading epochs from '{results_file}'") logger.info(f"Reading epochs from '{results_file}'")
with results_file.open('r') as f: with results_file.open("r") as f:
data = [] data = []
for line in f: for line in f:
data += [rapidjson.loads(line)] data += [rapidjson.loads(line)]
@@ -131,7 +137,7 @@ class HyperoptTools:
@staticmethod @staticmethod
def _test_hyperopt_results_exist(results_file) -> bool: def _test_hyperopt_results_exist(results_file) -> bool:
if results_file.is_file() and results_file.stat().st_size > 0: if results_file.is_file() and results_file.stat().st_size > 0:
if results_file.suffix == '.pickle': if results_file.suffix == ".pickle":
raise OperationalException( raise OperationalException(
"Legacy hyperopt results are no longer supported." "Legacy hyperopt results are no longer supported."
"Please rerun hyperopt or use an older version to load this file." "Please rerun hyperopt or use an older version to load this file."
@@ -144,18 +150,18 @@ class HyperoptTools:
@staticmethod @staticmethod
def load_filtered_results(results_file: Path, config: Config) -> Tuple[List, int]: def load_filtered_results(results_file: Path, config: Config) -> Tuple[List, int]:
filteroptions = { filteroptions = {
'only_best': config.get('hyperopt_list_best', False), "only_best": config.get("hyperopt_list_best", False),
'only_profitable': config.get('hyperopt_list_profitable', False), "only_profitable": config.get("hyperopt_list_profitable", False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0), "filter_min_trades": config.get("hyperopt_list_min_trades", 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0), "filter_max_trades": config.get("hyperopt_list_max_trades", 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time'), "filter_min_avg_time": config.get("hyperopt_list_min_avg_time"),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time'), "filter_max_avg_time": config.get("hyperopt_list_max_avg_time"),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit'), "filter_min_avg_profit": config.get("hyperopt_list_min_avg_profit"),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit'), "filter_max_avg_profit": config.get("hyperopt_list_max_avg_profit"),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit'), "filter_min_total_profit": config.get("hyperopt_list_min_total_profit"),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit'), "filter_max_total_profit": config.get("hyperopt_list_max_total_profit"),
'filter_min_objective': config.get('hyperopt_list_min_objective'), "filter_min_objective": config.get("hyperopt_list_min_objective"),
'filter_max_objective': config.get('hyperopt_list_max_objective'), "filter_max_objective": config.get("hyperopt_list_max_objective"),
} }
if not HyperoptTools._test_hyperopt_results_exist(results_file): if not HyperoptTools._test_hyperopt_results_exist(results_file):
# No file found. # No file found.
@@ -165,10 +171,11 @@ class HyperoptTools:
epochs = [] epochs = []
total_epochs = 0 total_epochs = 0
for epochs_tmp in HyperoptTools._read_results(results_file): for epochs_tmp in HyperoptTools._read_results(results_file):
if total_epochs == 0 and epochs_tmp[0].get('is_best') is None: if total_epochs == 0 and epochs_tmp[0].get("is_best") is None:
raise OperationalException( raise OperationalException(
"The file with HyperoptTools results is incompatible with this version " "The file with HyperoptTools results is incompatible with this version "
"of Freqtrade and cannot be loaded.") "of Freqtrade and cannot be loaded."
)
total_epochs += len(epochs_tmp) total_epochs += len(epochs_tmp)
epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False) epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
@@ -180,13 +187,18 @@ class HyperoptTools:
return epochs, total_epochs return epochs, total_epochs
@staticmethod @staticmethod
def show_epoch_details(results, total_epochs: int, print_json: bool, def show_epoch_details(
no_header: bool = False, header_str: Optional[str] = None) -> None: results,
total_epochs: int,
print_json: bool,
no_header: bool = False,
header_str: Optional[str] = None,
) -> None:
""" """
Display details of the hyperopt result Display details of the hyperopt result
""" """
params = results.get('params_details', {}) params = results.get("params_details", {})
non_optimized = results.get('params_not_optimized', {}) non_optimized = results.get("params_not_optimized", {})
# Default header string # Default header string
if header_str is None: if header_str is None:
@@ -198,23 +210,34 @@ class HyperoptTools:
if print_json: if print_json:
result_dict: Dict = {} result_dict: Dict = {}
for s in ['buy', 'sell', 'protection', for s in [
'roi', 'stoploss', 'trailing', 'max_open_trades']: "buy",
"sell",
"protection",
"roi",
"stoploss",
"trailing",
"max_open_trades",
]:
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT)) print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT))
else: else:
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
non_optimized)
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
non_optimized)
HyperoptTools._params_pretty_print(params, 'protection',
"Protection hyperspace params:", non_optimized)
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
HyperoptTools._params_pretty_print( HyperoptTools._params_pretty_print(
params, 'max_open_trades', "Max Open Trades:", non_optimized) params, "buy", "Buy hyperspace params:", non_optimized
)
HyperoptTools._params_pretty_print(
params, "sell", "Sell hyperspace params:", non_optimized
)
HyperoptTools._params_pretty_print(
params, "protection", "Protection hyperspace params:", non_optimized
)
HyperoptTools._params_pretty_print(params, "roi", "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, "stoploss", "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, "trailing", "Trailing stop:", non_optimized)
HyperoptTools._params_pretty_print(
params, "max_open_trades", "Max Open Trades:", non_optimized
)
@staticmethod @staticmethod
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@@ -227,23 +250,23 @@ class HyperoptTools:
if len(space_non_optimized) > 0: if len(space_non_optimized) > 0:
all_space_params = {**space_params, **space_non_optimized} all_space_params = {**space_params, **space_non_optimized}
if space in ['buy', 'sell']: if space in ["buy", "sell"]:
result_dict.setdefault('params', {}).update(all_space_params) result_dict.setdefault("params", {}).update(all_space_params)
elif space == 'roi': elif space == "roi":
# Convert keys in min_roi dict to strings because # Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys... # rapidjson cannot dump dicts with integer keys...
result_dict['minimal_roi'] = {str(k): v for k, v in all_space_params.items()} result_dict["minimal_roi"] = {str(k): v for k, v in all_space_params.items()}
else: # 'stoploss', 'trailing' else: # 'stoploss', 'trailing'
result_dict.update(all_space_params) result_dict.update(all_space_params)
@staticmethod @staticmethod
def _params_pretty_print( def _params_pretty_print(
params, space: str, header: str, non_optimized: Optional[Dict] = None) -> None: params, space: str, header: str, non_optimized: Optional[Dict] = None
) -> None:
if space in params or (non_optimized and space in non_optimized): if space in params or (non_optimized and space in non_optimized):
space_params = HyperoptTools._space_params(params, space, 5) space_params = HyperoptTools._space_params(params, space, 5)
no_params = HyperoptTools._space_params(non_optimized, space, 5) no_params = HyperoptTools._space_params(non_optimized, space, 5)
appendix = '' appendix = ""
if not space_params and not no_params: if not space_params and not no_params:
# No parameters - don't print # No parameters - don't print
return return
@@ -254,15 +277,18 @@ class HyperoptTools:
result = f"\n# {header}\n" result = f"\n# {header}\n"
if space == "stoploss": if space == "stoploss":
stoploss = safe_value_fallback2(space_params, no_params, space, space) stoploss = safe_value_fallback2(space_params, no_params, space, space)
result += (f"stoploss = {stoploss}{appendix}") result += f"stoploss = {stoploss}{appendix}"
elif space == "max_open_trades": elif space == "max_open_trades":
max_open_trades = safe_value_fallback2(space_params, no_params, space, space) max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
result += (f"max_open_trades = {max_open_trades}{appendix}") result += f"max_open_trades = {max_open_trades}{appendix}"
elif space == "roi": elif space == "roi":
result = result[:-1] + f'{appendix}\n' result = result[:-1] + f"{appendix}\n"
minimal_roi_result = rapidjson.dumps({ minimal_roi_result = rapidjson.dumps(
str(k): v for k, v in (space_params or no_params).items() {str(k): v for k, v in (space_params or no_params).items()},
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE) default=str,
indent=4,
number_mode=rapidjson.NM_NATIVE,
)
result += f"minimal_roi = {minimal_roi_result}" result += f"minimal_roi = {minimal_roi_result}"
elif space == "trailing": elif space == "trailing":
for k, v in (space_params or no_params).items(): for k, v in (space_params or no_params).items():
@@ -291,177 +317,213 @@ class HyperoptTools:
""" """
p = params.copy() p = params.copy()
p.update(non_optimized) p.update(non_optimized)
result = '{\n' result = "{\n"
for k, param in p.items(): for k, param in p.items():
result += " " * indent + f'"{k}": ' result += " " * indent + f'"{k}": '
result += f'"{param}",' if isinstance(param, str) else f'{param},' result += f'"{param}",' if isinstance(param, str) else f"{param},"
if k in non_optimized: if k in non_optimized:
result += NON_OPT_PARAM_APPENDIX result += NON_OPT_PARAM_APPENDIX
result += "\n" result += "\n"
result += '}' result += "}"
return result return result
@staticmethod @staticmethod
def is_best_loss(results, current_best_loss: float) -> bool: def is_best_loss(results, current_best_loss: float) -> bool:
return bool(results['loss'] < current_best_loss) return bool(results["loss"] < current_best_loss)
@staticmethod @staticmethod
def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str: def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:
""" """
Return the formatted results explanation in a string Return the formatted results explanation in a string
""" """
return (f"{results_metrics['total_trades']:6d} trades. " return (
f"{results_metrics['wins']}/{results_metrics['draws']}" f"{results_metrics['total_trades']:6d} trades. "
f"/{results_metrics['losses']} Wins/Draws/Losses. " f"{results_metrics['wins']}/{results_metrics['draws']}"
f"Avg profit {results_metrics['profit_mean']:7.2%}. " f"/{results_metrics['losses']} Wins/Draws/Losses. "
f"Median profit {results_metrics['profit_median']:7.2%}. " f"Avg profit {results_metrics['profit_mean']:7.2%}. "
f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} " f"Median profit {results_metrics['profit_median']:7.2%}. "
f"({results_metrics['profit_total']:8.2%}). " f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
f"Avg duration {results_metrics['holding_avg']} min." f"({results_metrics['profit_total']:8.2%}). "
) f"Avg duration {results_metrics['holding_avg']} min."
)
@staticmethod @staticmethod
def _format_explanation_string(results, total_epochs) -> str: def _format_explanation_string(results, total_epochs) -> str:
return (("*" if results['is_initial_point'] else " ") + return (
f"{results['current_epoch']:5d}/{total_epochs}: " + ("*" if results["is_initial_point"] else " ")
f"{results['results_explanation']} " + + f"{results['current_epoch']:5d}/{total_epochs}: "
f"Objective: {results['loss']:.5f}") + f"{results['results_explanation']} "
+ f"Objective: {results['loss']:.5f}"
)
@staticmethod @staticmethod
def prepare_trials_columns(trials: pd.DataFrame, has_drawdown: bool) -> pd.DataFrame: def prepare_trials_columns(trials: pd.DataFrame, has_drawdown: bool) -> pd.DataFrame:
trials['Best'] = '' trials["Best"] = ""
if 'results_metrics.winsdrawslosses' not in trials.columns: if "results_metrics.winsdrawslosses" not in trials.columns:
# Ensure compatibility with older versions of hyperopt results # Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A' trials["results_metrics.winsdrawslosses"] = "N/A"
if not has_drawdown: if not has_drawdown:
# Ensure compatibility with older versions of hyperopt results # Ensure compatibility with older versions of hyperopt results
trials['results_metrics.max_drawdown_account'] = None trials["results_metrics.max_drawdown_account"] = None
if 'is_random' not in trials.columns: if "is_random" not in trials.columns:
trials['is_random'] = False trials["is_random"] = False
# New mode, using backtest result for metrics # New mode, using backtest result for metrics
trials['results_metrics.winsdrawslosses'] = trials.apply( trials["results_metrics.winsdrawslosses"] = trials.apply(
lambda x: generate_wins_draws_losses( lambda x: generate_wins_draws_losses(
x['results_metrics.wins'], x['results_metrics.draws'], x["results_metrics.wins"], x["results_metrics.draws"], x["results_metrics.losses"]
x['results_metrics.losses'] ),
), axis=1) axis=1,
)
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', trials = trials[
'results_metrics.winsdrawslosses', [
'results_metrics.profit_mean', 'results_metrics.profit_total_abs', "Best",
'results_metrics.profit_total', 'results_metrics.holding_avg', "current_epoch",
'results_metrics.max_drawdown', "results_metrics.total_trades",
'results_metrics.max_drawdown_account', 'results_metrics.max_drawdown_abs', "results_metrics.winsdrawslosses",
'loss', 'is_initial_point', 'is_random', 'is_best']] "results_metrics.profit_mean",
"results_metrics.profit_total_abs",
"results_metrics.profit_total",
"results_metrics.holding_avg",
"results_metrics.max_drawdown",
"results_metrics.max_drawdown_account",
"results_metrics.max_drawdown_abs",
"loss",
"is_initial_point",
"is_random",
"is_best",
]
]
trials.columns = [ trials.columns = [
'Best', 'Epoch', 'Trades', ' Win Draw Loss Win%', 'Avg profit', "Best",
'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account', "Epoch",
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best' "Trades",
] " Win Draw Loss Win%",
"Avg profit",
"Total profit",
"Profit",
"Avg duration",
"max_drawdown",
"max_drawdown_account",
"max_drawdown_abs",
"Objective",
"is_initial_point",
"is_random",
"is_best",
]
return trials return trials
@staticmethod @staticmethod
def get_result_table(config: Config, results: list, total_epochs: int, highlight_best: bool, def get_result_table(
print_colorized: bool, remove_header: int) -> str: config: Config,
results: list,
total_epochs: int,
highlight_best: bool,
print_colorized: bool,
remove_header: int,
) -> str:
""" """
Log result table Log result table
""" """
if not results: if not results:
return '' return ""
tabulate.PRESERVE_WHITESPACE = True tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1) trials = json_normalize(results, max_level=1)
has_account_drawdown = 'results_metrics.max_drawdown_account' in trials.columns has_account_drawdown = "results_metrics.max_drawdown_account" in trials.columns
trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown) trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown)
trials['is_profit'] = False trials["is_profit"] = False
trials.loc[trials['is_initial_point'] | trials['is_random'], 'Best'] = '* ' trials.loc[trials["is_initial_point"] | trials["is_random"], "Best"] = "* "
trials.loc[trials['is_best'], 'Best'] = 'Best' trials.loc[trials["is_best"], "Best"] = "Best"
trials.loc[ trials.loc[
(trials['is_initial_point'] | trials['is_random']) & trials['is_best'], (trials["is_initial_point"] | trials["is_random"]) & trials["is_best"], "Best"
'Best'] = '* Best' ] = "* Best"
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials.loc[trials["Total profit"] > 0, "is_profit"] = True
trials['Trades'] = trials['Trades'].astype(str) trials["Trades"] = trials["Trades"].astype(str)
# perc_multi = 1 if legacy_mode else 100 # perc_multi = 1 if legacy_mode else 100
trials['Epoch'] = trials['Epoch'].apply( trials["Epoch"] = trials["Epoch"].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) lambda x: "{}/{}".format(str(x).rjust(len(str(total_epochs)), " "), total_epochs)
) )
trials['Avg profit'] = trials['Avg profit'].apply( trials["Avg profit"] = trials["Avg profit"].apply(
lambda x: f'{x:,.2%}'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') lambda x: f"{x:,.2%}".rjust(7, " ") if not isna(x) else "--".rjust(7, " ")
) )
trials['Avg duration'] = trials['Avg duration'].apply( trials["Avg duration"] = trials["Avg duration"].apply(
lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}" lambda x: f"{x:,.1f} m".rjust(7, " ")
if not isna(x) else "--".rjust(7, ' ') if isinstance(x, float)
else f"{x}"
if not isna(x)
else "--".rjust(7, " ")
) )
trials['Objective'] = trials['Objective'].apply( trials["Objective"] = trials["Objective"].apply(
lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') lambda x: f"{x:,.5f}".rjust(8, " ") if x != 100000 else "N/A".rjust(8, " ")
) )
stake_currency = config['stake_currency'] stake_currency = config["stake_currency"]
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply( trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
lambda x: "{} {}".format( lambda x: "{} {}".format(
fmt_coin(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True), fmt_coin(x["max_drawdown_abs"], stake_currency, keep_trailing_zeros=True),
(f"({x['max_drawdown_account']:,.2%})" (
f"({x['max_drawdown_account']:,.2%})"
if has_account_drawdown if has_account_drawdown
else f"({x['max_drawdown']:,.2%})" else f"({x['max_drawdown']:,.2%})"
).rjust(10, ' ') ).rjust(10, " "),
).rjust(25 + len(stake_currency)) ).rjust(25 + len(stake_currency))
if x['max_drawdown'] != 0.0 or x['max_drawdown_account'] != 0.0 if x["max_drawdown"] != 0.0 or x["max_drawdown_account"] != 0.0
else '--'.rjust(25 + len(stake_currency)), else "--".rjust(25 + len(stake_currency)),
axis=1 axis=1,
) )
trials = trials.drop(columns=['max_drawdown_abs', 'max_drawdown', 'max_drawdown_account']) trials = trials.drop(columns=["max_drawdown_abs", "max_drawdown", "max_drawdown_account"])
trials['Profit'] = trials.apply( trials["Profit"] = trials.apply(
lambda x: '{} {}'.format( lambda x: "{} {}".format(
fmt_coin(x['Total profit'], stake_currency, keep_trailing_zeros=True), fmt_coin(x["Total profit"], stake_currency, keep_trailing_zeros=True),
f"({x['Profit']:,.2%})".rjust(10, ' ') f"({x['Profit']:,.2%})".rjust(10, " "),
).rjust(25 + len(stake_currency)) ).rjust(25 + len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25 + len(stake_currency)), if x["Total profit"] != 0.0
axis=1 else "--".rjust(25 + len(stake_currency)),
axis=1,
) )
trials = trials.drop(columns=['Total profit']) trials = trials.drop(columns=["Total profit"])
if print_colorized: if print_colorized:
trials2 = trials.astype(str) trials2 = trials.astype(str)
for i in range(len(trials)): for i in range(len(trials)):
if trials.loc[i]['is_profit']: if trials.loc[i]["is_profit"]:
for j in range(len(trials.loc[i]) - 3): for j in range(len(trials.loc[i]) - 3):
trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[i, j])}{Fore.RESET}" trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[i, j])}{Fore.RESET}"
if trials.loc[i]['is_best'] and highlight_best: if trials.loc[i]["is_best"] and highlight_best:
for j in range(len(trials.loc[i]) - 3): for j in range(len(trials.loc[i]) - 3):
trials2.iat[i, j] = ( trials2.iat[i, j] = (
f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}" f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}"
) )
trials = trials2 trials = trials2
del trials2 del trials2
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random']) trials = trials.drop(columns=["is_initial_point", "is_best", "is_profit", "is_random"])
if remove_header > 0: if remove_header > 0:
table = tabulate.tabulate( table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='orgtbl', trials.to_dict(orient="list"), tablefmt="orgtbl", headers="keys", stralign="right"
headers='keys', stralign="right"
) )
table = table.split("\n", remove_header)[remove_header] table = table.split("\n", remove_header)[remove_header]
elif remove_header < 0: elif remove_header < 0:
table = tabulate.tabulate( table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql', trials.to_dict(orient="list"), tablefmt="psql", headers="keys", stralign="right"
headers='keys', stralign="right"
) )
table = "\n".join(table.split("\n")[0:remove_header]) table = "\n".join(table.split("\n")[0:remove_header])
else: else:
table = tabulate.tabulate( table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql', trials.to_dict(orient="list"), tablefmt="psql", headers="keys", stralign="right"
headers='keys', stralign="right"
) )
return table return table
@@ -479,56 +541,75 @@ class HyperoptTools:
return return
try: try:
Path(csv_file).open('w+').close() Path(csv_file).open("w+").close()
except OSError: except OSError:
logger.error(f"Failed to create CSV file: {csv_file}") logger.error(f"Failed to create CSV file: {csv_file}")
return return
trials = json_normalize(results, max_level=1) trials = json_normalize(results, max_level=1)
trials['Best'] = '' trials["Best"] = ""
trials['Stake currency'] = config['stake_currency'] trials["Stake currency"] = config["stake_currency"]
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', base_metrics = [
'results_metrics.profit_mean', 'results_metrics.profit_median', "Best",
'results_metrics.profit_total', 'Stake currency', "current_epoch",
'results_metrics.profit_total_abs', 'results_metrics.holding_avg', "results_metrics.total_trades",
'results_metrics.trade_count_long', 'results_metrics.trade_count_short', "results_metrics.profit_mean",
'loss', 'is_initial_point', 'is_best'] "results_metrics.profit_median",
"results_metrics.profit_total",
"Stake currency",
"results_metrics.profit_total_abs",
"results_metrics.holding_avg",
"results_metrics.trade_count_long",
"results_metrics.trade_count_short",
"loss",
"is_initial_point",
"is_best",
]
perc_multi = 100 perc_multi = 100
param_metrics = [("params_dict." + param) for param in results[0]['params_dict'].keys()] param_metrics = [("params_dict." + param) for param in results[0]["params_dict"].keys()]
trials = trials[base_metrics + param_metrics] trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', base_columns = [
'Stake currency', 'Profit', 'Avg duration', "Best",
'Trade count long', 'Trade count short', "Epoch",
'Objective', "Trades",
'is_initial_point', 'is_best'] "Avg profit",
param_columns = list(results[0]['params_dict'].keys()) "Median profit",
"Total profit",
"Stake currency",
"Profit",
"Avg duration",
"Trade count long",
"Trade count short",
"Objective",
"is_initial_point",
"is_best",
]
param_columns = list(results[0]["params_dict"].keys())
trials.columns = base_columns + param_columns trials.columns = base_columns + param_columns
trials['is_profit'] = False trials["is_profit"] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*' trials.loc[trials["is_initial_point"], "Best"] = "*"
trials.loc[trials['is_best'], 'Best'] = 'Best' trials.loc[trials["is_best"], "Best"] = "Best"
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' trials.loc[trials["is_initial_point"] & trials["is_best"], "Best"] = "* Best"
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials.loc[trials["Total profit"] > 0, "is_profit"] = True
trials['Epoch'] = trials['Epoch'].astype(str) trials["Epoch"] = trials["Epoch"].astype(str)
trials['Trades'] = trials['Trades'].astype(str) trials["Trades"] = trials["Trades"].astype(str)
trials['Median profit'] = trials['Median profit'] * perc_multi trials["Median profit"] = trials["Median profit"] * perc_multi
trials['Total profit'] = trials['Total profit'].apply( trials["Total profit"] = trials["Total profit"].apply(
lambda x: f'{x:,.8f}' if x != 0.0 else "" lambda x: f"{x:,.8f}" if x != 0.0 else ""
) )
trials['Profit'] = trials['Profit'].apply( trials["Profit"] = trials["Profit"].apply(lambda x: f"{x:,.2f}" if not isna(x) else "")
lambda x: f'{x:,.2f}' if not isna(x) else "" trials["Avg profit"] = trials["Avg profit"].apply(
lambda x: f"{x * perc_multi:,.2f}%" if not isna(x) else ""
) )
trials['Avg profit'] = trials['Avg profit'].apply( trials["Objective"] = trials["Objective"].apply(
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" lambda x: f"{x:,.5f}" if x != 100000 else ""
)
trials['Objective'] = trials['Objective'].apply(
lambda x: f'{x:,.5f}' if x != 100000 else ""
) )
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) trials = trials.drop(columns=["is_initial_point", "is_best", "is_profit"])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') trials.to_csv(csv_file, index=False, header=True, mode="w", encoding="UTF-8")
logger.info(f"CSV file created: {csv_file}") logger.info(f"CSV file created: {csv_file}")