feat: split hyperopt class
this ensures it's clear which parts are passed to workers
This commit is contained in:
@@ -7,29 +7,24 @@ This module contains the hyperopt logic
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
from datetime import datetime
|
||||||
from datetime import datetime, timezone
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects
|
||||||
from joblib.externals import cloudpickle
|
from joblib.externals import cloudpickle
|
||||||
from pandas import DataFrame
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
|
from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
|
||||||
from freqtrade.data.converter import trim_dataframes
|
|
||||||
from freqtrade.data.history import get_timerange
|
|
||||||
from freqtrade.data.metrics import calculate_market_change
|
|
||||||
from freqtrade.enums import HyperoptState
|
from freqtrade.enums import HyperoptState
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
from freqtrade.misc import file_dump_json, plural
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
|
||||||
|
|
||||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||||
from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto
|
from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto
|
||||||
|
from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer
|
||||||
from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput
|
from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput
|
||||||
from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss
|
from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss
|
||||||
from freqtrade.optimize.hyperopt_tools import (
|
from freqtrade.optimize.hyperopt_tools import (
|
||||||
@@ -37,17 +32,9 @@ from freqtrade.optimize.hyperopt_tools import (
|
|||||||
HyperoptTools,
|
HyperoptTools,
|
||||||
hyperopt_serializer,
|
hyperopt_serializer,
|
||||||
)
|
)
|
||||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
|
||||||
from freqtrade.util import get_progress_tracker
|
from freqtrade.util import get_progress_tracker
|
||||||
|
|
||||||
|
|
||||||
# Suppress scikit-learn FutureWarnings from skopt
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
|
||||||
from skopt import Optimizer
|
|
||||||
from skopt.space import Dimension
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,8 +44,6 @@ INITIAL_POINTS = 30
|
|||||||
# in the skopt model queue, to optimize memory consumption
|
# in the skopt model queue, to optimize memory consumption
|
||||||
SKOPT_MODEL_QUEUE_SIZE = 10
|
SKOPT_MODEL_QUEUE_SIZE = 10
|
||||||
|
|
||||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
|
||||||
|
|
||||||
|
|
||||||
class Hyperopt:
|
class Hyperopt:
|
||||||
"""
|
"""
|
||||||
@@ -70,43 +55,19 @@ class Hyperopt:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
self.buy_space: list[Dimension] = []
|
|
||||||
self.sell_space: list[Dimension] = []
|
|
||||||
self.protection_space: list[Dimension] = []
|
|
||||||
self.roi_space: list[Dimension] = []
|
|
||||||
self.stoploss_space: list[Dimension] = []
|
|
||||||
self.trailing_space: list[Dimension] = []
|
|
||||||
self.max_open_trades_space: list[Dimension] = []
|
|
||||||
self.dimensions: list[Dimension] = []
|
|
||||||
|
|
||||||
self._hyper_out: HyperoptOutput = HyperoptOutput(streaming=True)
|
self._hyper_out: HyperoptOutput = HyperoptOutput(streaming=True)
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.min_date: datetime
|
|
||||||
self.max_date: datetime
|
|
||||||
|
|
||||||
self.backtesting = Backtesting(self.config)
|
|
||||||
self.pairlist = self.backtesting.pairlists.whitelist
|
|
||||||
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 self.config.get("hyperopt"):
|
||||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
|
||||||
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.custom_hyperopt.strategy = self.backtesting.strategy
|
|
||||||
|
|
||||||
self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__)
|
|
||||||
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
|
|
||||||
self.config
|
|
||||||
)
|
|
||||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
|
||||||
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
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.results_file: Path = (
|
||||||
@@ -123,7 +84,6 @@ class Hyperopt:
|
|||||||
|
|
||||||
self.clean_hyperopt()
|
self.clean_hyperopt()
|
||||||
|
|
||||||
self.market_change = 0.0
|
|
||||||
self.num_epochs_saved = 0
|
self.num_epochs_saved = 0
|
||||||
self.current_best_epoch: dict[str, Any] | None = None
|
self.current_best_epoch: dict[str, Any] | None = None
|
||||||
|
|
||||||
@@ -136,6 +96,8 @@ class Hyperopt:
|
|||||||
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)
|
||||||
|
|
||||||
|
self.hyperopter = HyperOptimizer(self.config)
|
||||||
|
|
||||||
@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")
|
||||||
@@ -161,18 +123,6 @@ class Hyperopt:
|
|||||||
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[str, Any]:
|
|
||||||
# Ensure the number of dimensions match
|
|
||||||
# the number of parameters in the list.
|
|
||||||
if len(raw_params) != len(dimensions):
|
|
||||||
raise ValueError("Mismatch in number of search-space dimensions.")
|
|
||||||
|
|
||||||
# Return a dict where the keys are the names of the dimensions
|
|
||||||
# and the values are taken from the list of parameters.
|
|
||||||
return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)}
|
|
||||||
|
|
||||||
def _save_result(self, epoch: dict) -> None:
|
def _save_result(self, epoch: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Save hyperopt results to file
|
Save hyperopt results to file
|
||||||
@@ -199,58 +149,6 @@ class Hyperopt:
|
|||||||
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)}, log=False)
|
file_dump_json(latest_filename, {"latest_hyperopt": str(self.results_file.name)}, log=False)
|
||||||
|
|
||||||
def _get_params_details(self, params: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Return the params for each space
|
|
||||||
"""
|
|
||||||
result: dict = {}
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "buy"):
|
|
||||||
result["buy"] = {p.name: params.get(p.name) for p in self.buy_space}
|
|
||||||
if HyperoptTools.has_space(self.config, "sell"):
|
|
||||||
result["sell"] = {p.name: params.get(p.name) for p in self.sell_space}
|
|
||||||
if HyperoptTools.has_space(self.config, "protection"):
|
|
||||||
result["protection"] = {p.name: params.get(p.name) for p in self.protection_space}
|
|
||||||
if HyperoptTools.has_space(self.config, "roi"):
|
|
||||||
result["roi"] = {
|
|
||||||
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, "trailing"):
|
|
||||||
result["trailing"] = self.custom_hyperopt.generate_trailing_params(params)
|
|
||||||
if HyperoptTools.has_space(self.config, "trades"):
|
|
||||||
result["max_open_trades"] = {
|
|
||||||
"max_open_trades": (
|
|
||||||
self.backtesting.strategy.max_open_trades
|
|
||||||
if self.backtesting.strategy.max_open_trades != float("inf")
|
|
||||||
else -1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _get_no_optimize_details(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Get non-optimized parameters
|
|
||||||
"""
|
|
||||||
result: dict[str, Any] = {}
|
|
||||||
strategy = self.backtesting.strategy
|
|
||||||
if not HyperoptTools.has_space(self.config, "roi"):
|
|
||||||
result["roi"] = {str(k): v for k, v in strategy.minimal_roi.items()}
|
|
||||||
if not HyperoptTools.has_space(self.config, "stoploss"):
|
|
||||||
result["stoploss"] = {"stoploss": strategy.stoploss}
|
|
||||||
if not HyperoptTools.has_space(self.config, "trailing"):
|
|
||||||
result["trailing"] = {
|
|
||||||
"trailing_stop": strategy.trailing_stop,
|
|
||||||
"trailing_stop_positive": strategy.trailing_stop_positive,
|
|
||||||
"trailing_stop_positive_offset": strategy.trailing_stop_positive_offset,
|
|
||||||
"trailing_only_offset_is_reached": strategy.trailing_only_offset_is_reached,
|
|
||||||
}
|
|
||||||
if not HyperoptTools.has_space(self.config, "trades"):
|
|
||||||
result["max_open_trades"] = {"max_open_trades": strategy.max_open_trades}
|
|
||||||
return result
|
|
||||||
|
|
||||||
def print_results(self, results: dict[str, Any]) -> None:
|
def print_results(self, results: dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Log results if it is better than any previous evaluation
|
Log results if it is better than any previous evaluation
|
||||||
@@ -266,258 +164,16 @@ class Hyperopt:
|
|||||||
self.print_all,
|
self.print_all,
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_spaces(self):
|
|
||||||
"""
|
|
||||||
Assign the dimensions in the hyperoptimization space.
|
|
||||||
"""
|
|
||||||
if HyperoptTools.has_space(self.config, "protection"):
|
|
||||||
# Protections can only be optimized when using the Parameter interface
|
|
||||||
logger.debug("Hyperopt has 'protection' space")
|
|
||||||
# Enable Protections if protection space is selected.
|
|
||||||
self.config["enable_protections"] = True
|
|
||||||
self.backtesting.enable_protections = True
|
|
||||||
self.protection_space = self.custom_hyperopt.protection_space()
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "buy"):
|
|
||||||
logger.debug("Hyperopt has 'buy' space")
|
|
||||||
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "sell"):
|
|
||||||
logger.debug("Hyperopt has 'sell' space")
|
|
||||||
self.sell_space = self.custom_hyperopt.sell_indicator_space()
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "roi"):
|
|
||||||
logger.debug("Hyperopt has 'roi' space")
|
|
||||||
self.roi_space = self.custom_hyperopt.roi_space()
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "stoploss"):
|
|
||||||
logger.debug("Hyperopt has 'stoploss' space")
|
|
||||||
self.stoploss_space = self.custom_hyperopt.stoploss_space()
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "trailing"):
|
|
||||||
logger.debug("Hyperopt has 'trailing' space")
|
|
||||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "trades"):
|
|
||||||
logger.debug("Hyperopt has '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.roi_space
|
|
||||||
+ self.stoploss_space
|
|
||||||
+ self.trailing_space
|
|
||||||
+ self.max_open_trades_space
|
|
||||||
)
|
|
||||||
|
|
||||||
def assign_params(self, params_dict: dict[str, Any], category: str) -> None:
|
|
||||||
"""
|
|
||||||
Assign hyperoptable parameters
|
|
||||||
"""
|
|
||||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
|
||||||
if attr.optimize:
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
attr.value = params_dict[attr_name]
|
|
||||||
|
|
||||||
def generate_optimizer(self, raw_params: list[Any]) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Used Optimize function.
|
|
||||||
Called once per epoch to optimize whatever is configured.
|
|
||||||
Keep this function as optimized as possible!
|
|
||||||
"""
|
|
||||||
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
|
||||||
backtest_start_time = datetime.now(timezone.utc)
|
|
||||||
params_dict = self._get_params_dict(self.dimensions, raw_params)
|
|
||||||
|
|
||||||
# Apply parameters
|
|
||||||
if HyperoptTools.has_space(self.config, "buy"):
|
|
||||||
self.assign_params(params_dict, "buy")
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "sell"):
|
|
||||||
self.assign_params(params_dict, "sell")
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "protection"):
|
|
||||||
self.assign_params(params_dict, "protection")
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "roi"):
|
|
||||||
self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(
|
|
||||||
params_dict
|
|
||||||
)
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "stoploss"):
|
|
||||||
self.backtesting.strategy.stoploss = params_dict["stoploss"]
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "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"]
|
|
||||||
self.backtesting.strategy.trailing_stop_positive_offset = d[
|
|
||||||
"trailing_stop_positive_offset"
|
|
||||||
]
|
|
||||||
self.backtesting.strategy.trailing_only_offset_is_reached = d[
|
|
||||||
"trailing_only_offset_is_reached"
|
|
||||||
]
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, "trades"):
|
|
||||||
if self.config["stake_amount"] == "unlimited" and (
|
|
||||||
params_dict["max_open_trades"] == -1 or params_dict["max_open_trades"] == 0
|
|
||||||
):
|
|
||||||
# Ignore unlimited max open trades if stake amount is unlimited
|
|
||||||
params_dict.update({"max_open_trades": self.config["max_open_trades"]})
|
|
||||||
|
|
||||||
updated_max_open_trades = (
|
|
||||||
int(params_dict["max_open_trades"])
|
|
||||||
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.backtesting.strategy.max_open_trades = updated_max_open_trades
|
|
||||||
|
|
||||||
with self.data_pickle_file.open("rb") as f:
|
|
||||||
processed = load(f, mmap_mode="r")
|
|
||||||
if self.analyze_per_epoch:
|
|
||||||
# Data is not yet analyzed, rerun populate_indicators.
|
|
||||||
processed = self.advise_and_trim(processed)
|
|
||||||
|
|
||||||
bt_results = self.backtesting.backtest(
|
|
||||||
processed=processed, start_date=self.min_date, end_date=self.max_date
|
|
||||||
)
|
|
||||||
backtest_end_time = datetime.now(timezone.utc)
|
|
||||||
bt_results.update(
|
|
||||||
{
|
|
||||||
"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, params_dict, processed=processed
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_results_dict(
|
|
||||||
self,
|
|
||||||
backtesting_results: dict[str, Any],
|
|
||||||
min_date: datetime,
|
|
||||||
max_date: datetime,
|
|
||||||
params_dict: dict[str, Any],
|
|
||||||
processed: dict[str, DataFrame],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
params_details = self._get_params_details(params_dict)
|
|
||||||
|
|
||||||
strat_stats = generate_strategy_stats(
|
|
||||||
self.pairlist,
|
|
||||||
self.backtesting.strategy.get_strategy_name(),
|
|
||||||
backtesting_results,
|
|
||||||
min_date,
|
|
||||||
max_date,
|
|
||||||
market_change=self.market_change,
|
|
||||||
is_hyperopt=True,
|
|
||||||
)
|
|
||||||
results_explanation = HyperoptTools.format_results_explanation_string(
|
|
||||||
strat_stats, self.config["stake_currency"]
|
|
||||||
)
|
|
||||||
|
|
||||||
not_optimized = self.backtesting.strategy.get_no_optimize_params()
|
|
||||||
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
|
|
||||||
|
|
||||||
trade_count = strat_stats["total_trades"]
|
|
||||||
total_profit = strat_stats["profit_total"]
|
|
||||||
|
|
||||||
# If this evaluation contains too short amount of trades to be
|
|
||||||
# interesting -- consider it as 'bad' (assigned max. loss value)
|
|
||||||
# in order to cast this hyperspace point away from optimization
|
|
||||||
# path. We do not want to optimize 'hodl' strategies.
|
|
||||||
loss: float = MAX_LOSS
|
|
||||||
if trade_count >= self.config["hyperopt_min_trades"]:
|
|
||||||
loss = self.calculate_loss(
|
|
||||||
results=backtesting_results["results"],
|
|
||||||
trade_count=trade_count,
|
|
||||||
min_date=min_date,
|
|
||||||
max_date=max_date,
|
|
||||||
config=self.config,
|
|
||||||
processed=processed,
|
|
||||||
backtest_stats=strat_stats,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"loss": loss,
|
|
||||||
"params_dict": params_dict,
|
|
||||||
"params_details": params_details,
|
|
||||||
"params_not_optimized": not_optimized,
|
|
||||||
"results_metrics": strat_stats,
|
|
||||||
"results_explanation": results_explanation,
|
|
||||||
"total_profit": total_profit,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_optimizer(self, dimensions: list[Dimension], cpu_count) -> Optimizer:
|
|
||||||
estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions)
|
|
||||||
|
|
||||||
acq_optimizer = "sampling"
|
|
||||||
if isinstance(estimator, str):
|
|
||||||
if estimator not in ("GP", "RF", "ET", "GBRT"):
|
|
||||||
raise OperationalException(f"Estimator {estimator} not supported.")
|
|
||||||
else:
|
|
||||||
acq_optimizer = "auto"
|
|
||||||
|
|
||||||
logger.info(f"Using estimator {estimator}.")
|
|
||||||
return Optimizer(
|
|
||||||
dimensions,
|
|
||||||
base_estimator=estimator,
|
|
||||||
acq_optimizer=acq_optimizer,
|
|
||||||
n_initial_points=INITIAL_POINTS,
|
|
||||||
acq_optimizer_kwargs={"n_jobs": cpu_count},
|
|
||||||
random_state=self.random_state,
|
|
||||||
model_queue_size=SKOPT_MODEL_QUEUE_SIZE,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run_optimizer_parallel(self, parallel: Parallel, asked: list[list]) -> list[dict[str, Any]]:
|
def run_optimizer_parallel(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(wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked
|
delayed(wrap_non_picklable_objects(self.hyperopter.generate_optimizer))(v)
|
||||||
|
for v in asked
|
||||||
)
|
)
|
||||||
|
|
||||||
def _set_random_state(self, random_state: int | None) -> int:
|
def _set_random_state(self, random_state: int | None) -> int:
|
||||||
return random_state or random.randint(1, 2**16 - 1) # noqa: S311
|
return random_state or random.randint(1, 2**16 - 1) # noqa: S311
|
||||||
|
|
||||||
def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]:
|
|
||||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
|
||||||
|
|
||||||
# Trim startup period from analyzed dataframe to get correct dates for output.
|
|
||||||
# This is only used to keep track of min/max date after trimming.
|
|
||||||
# The result is NOT returned from this method, actual trimming happens in backtesting.
|
|
||||||
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
|
|
||||||
self.min_date, self.max_date = get_timerange(trimmed)
|
|
||||||
if not self.market_change:
|
|
||||||
self.market_change = calculate_market_change(trimmed, "close")
|
|
||||||
|
|
||||||
# Real trimming will happen as part of backtesting.
|
|
||||||
return preprocessed
|
|
||||||
|
|
||||||
def prepare_hyperopt_data(self) -> None:
|
|
||||||
HyperoptStateContainer.set_state(HyperoptState.DATALOAD)
|
|
||||||
data, self.timerange = self.backtesting.load_bt_data()
|
|
||||||
self.backtesting.load_bt_data_detail()
|
|
||||||
logger.info("Dataload complete. Calculating indicators")
|
|
||||||
|
|
||||||
if not self.analyze_per_epoch:
|
|
||||||
HyperoptStateContainer.set_state(HyperoptState.INDICATORS)
|
|
||||||
|
|
||||||
preprocessed = self.advise_and_trim(data)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Hyperopting with data from "
|
|
||||||
f"{self.min_date.strftime(DATETIME_PRINT_FORMAT)} "
|
|
||||||
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.
|
|
||||||
dump(preprocessed, self.data_pickle_file)
|
|
||||||
else:
|
|
||||||
dump(data, self.data_pickle_file)
|
|
||||||
|
|
||||||
def get_asked_points(self, n_points: int) -> tuple[list[list[Any]], list[bool]]:
|
def get_asked_points(self, n_points: int) -> tuple[list[list[Any]], list[bool]]:
|
||||||
"""
|
"""
|
||||||
Enforce points returned from `self.opt.ask` have not been already evaluated
|
Enforce points returned from `self.opt.ask` have not been already evaluated
|
||||||
@@ -595,27 +251,16 @@ class Hyperopt:
|
|||||||
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 ...
|
self.hyperopter.prepare_hyperopt()
|
||||||
self.init_spaces()
|
|
||||||
|
|
||||||
self.prepare_hyperopt_data()
|
|
||||||
|
|
||||||
# We don't need exchange instance anymore while running hyperopt
|
|
||||||
self.backtesting.exchange.close()
|
|
||||||
self.backtesting.exchange._api = None
|
|
||||||
self.backtesting.exchange._api_async = None
|
|
||||||
self.backtesting.exchange.loop = None # type: ignore
|
|
||||||
self.backtesting.exchange._loop_lock = None # type: ignore
|
|
||||||
self.backtesting.exchange._cache_lock = None # type: ignore
|
|
||||||
# self.backtesting.exchange = None # type: ignore
|
|
||||||
self.backtesting.pairlists = None # type: ignore
|
|
||||||
|
|
||||||
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.hyperopter.get_optimizer(
|
||||||
|
config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Parallel(n_jobs=config_jobs) as parallel:
|
with Parallel(n_jobs=config_jobs) as parallel:
|
||||||
@@ -638,7 +283,7 @@ class Hyperopt:
|
|||||||
# First analysis not in parallel mode when using --analyze-per-epoch.
|
# First analysis not in parallel mode when using --analyze-per-epoch.
|
||||||
# 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.hyperopter.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)
|
||||||
@@ -672,7 +317,9 @@ class Hyperopt:
|
|||||||
|
|
||||||
if self.current_best_epoch:
|
if self.current_best_epoch:
|
||||||
HyperoptTools.try_export_params(
|
HyperoptTools.try_export_params(
|
||||||
self.config, self.backtesting.strategy.get_strategy_name(), self.current_best_epoch
|
self.config,
|
||||||
|
self.hyperopter.get_strategy_name(),
|
||||||
|
self.current_best_epoch,
|
||||||
)
|
)
|
||||||
|
|
||||||
HyperoptTools.show_epoch_details(
|
HyperoptTools.show_epoch_details(
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
# pragma pylint: disable=too-many-instance-attributes, pointless-string-statement
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains the hyperopt logic
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from joblib import dump, load
|
||||||
|
from joblib.externals import cloudpickle
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
||||||
|
from freqtrade.data.converter import trim_dataframes
|
||||||
|
from freqtrade.data.history import get_timerange
|
||||||
|
from freqtrade.data.metrics import calculate_market_change
|
||||||
|
from freqtrade.enums import HyperoptState
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
|
||||||
|
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||||
|
from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto
|
||||||
|
from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss
|
||||||
|
from freqtrade.optimize.hyperopt_tools import (
|
||||||
|
HyperoptStateContainer,
|
||||||
|
HyperoptTools,
|
||||||
|
)
|
||||||
|
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||||
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
|
|
||||||
|
|
||||||
|
# Suppress scikit-learn FutureWarnings from skopt
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||||
|
from skopt import Optimizer
|
||||||
|
from skopt.space import Dimension
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||||
|
|
||||||
|
|
||||||
|
class HyperOptimizer:
|
||||||
|
"""
|
||||||
|
HyperoptOptimizer class
|
||||||
|
This class is sent to the hyperopt worker processes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: Config) -> None:
|
||||||
|
self.buy_space: list[Dimension] = []
|
||||||
|
self.sell_space: list[Dimension] = []
|
||||||
|
self.protection_space: list[Dimension] = []
|
||||||
|
self.roi_space: list[Dimension] = []
|
||||||
|
self.stoploss_space: list[Dimension] = []
|
||||||
|
self.trailing_space: list[Dimension] = []
|
||||||
|
self.max_open_trades_space: list[Dimension] = []
|
||||||
|
self.dimensions: list[Dimension] = []
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
self.min_date: datetime
|
||||||
|
self.max_date: datetime
|
||||||
|
|
||||||
|
self.backtesting = Backtesting(self.config)
|
||||||
|
self.pairlist = self.backtesting.pairlists.whitelist
|
||||||
|
self.custom_hyperopt: HyperOptAuto
|
||||||
|
self.analyze_per_epoch = self.config.get("analyze_per_epoch", False)
|
||||||
|
|
||||||
|
if not self.config.get("hyperopt"):
|
||||||
|
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||||
|
else:
|
||||||
|
raise OperationalException(
|
||||||
|
"Using separate Hyperopt files has been removed in 2021.9. Please convert "
|
||||||
|
"your existing Hyperopt file to the new Hyperoptable strategy interface"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||||
|
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||||
|
|
||||||
|
self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__)
|
||||||
|
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
|
||||||
|
self.config
|
||||||
|
)
|
||||||
|
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||||
|
|
||||||
|
self.data_pickle_file = (
|
||||||
|
self.config["user_data_dir"] / "hyperopt_results" / "hyperopt_tickerdata.pkl"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.market_change = 0.0
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "sell"):
|
||||||
|
# Make sure use_exit_signal is enabled
|
||||||
|
self.config["use_exit_signal"] = True
|
||||||
|
|
||||||
|
def prepare_hyperopt(self) -> None:
|
||||||
|
# Initialize spaces ...
|
||||||
|
self.init_spaces()
|
||||||
|
|
||||||
|
self.prepare_hyperopt_data()
|
||||||
|
|
||||||
|
# We don't need exchange instance anymore while running hyperopt
|
||||||
|
self.backtesting.exchange.close()
|
||||||
|
self.backtesting.exchange._api = None
|
||||||
|
self.backtesting.exchange._api_async = None
|
||||||
|
self.backtesting.exchange.loop = None # type: ignore
|
||||||
|
self.backtesting.exchange._loop_lock = None # type: ignore
|
||||||
|
self.backtesting.exchange._cache_lock = None # type: ignore
|
||||||
|
# self.backtesting.exchange = None # type: ignore
|
||||||
|
self.backtesting.pairlists = None # type: ignore
|
||||||
|
|
||||||
|
def get_strategy_name(self) -> str:
|
||||||
|
return self.backtesting.strategy.get_strategy_name()
|
||||||
|
|
||||||
|
def hyperopt_pickle_magic(self, bases) -> None:
|
||||||
|
"""
|
||||||
|
Hyperopt magic to allow strategy inheritance across files.
|
||||||
|
For this to properly work, we need to register the module of the imported class
|
||||||
|
to pickle as value.
|
||||||
|
"""
|
||||||
|
for modules in bases:
|
||||||
|
if modules.__name__ != "IStrategy":
|
||||||
|
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
|
||||||
|
self.hyperopt_pickle_magic(modules.__bases__)
|
||||||
|
|
||||||
|
def _get_params_dict(
|
||||||
|
self, dimensions: list[Dimension], raw_params: list[Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
# Ensure the number of dimensions match
|
||||||
|
# the number of parameters in the list.
|
||||||
|
if len(raw_params) != len(dimensions):
|
||||||
|
raise ValueError("Mismatch in number of search-space dimensions.")
|
||||||
|
|
||||||
|
# Return a dict where the keys are the names of the dimensions
|
||||||
|
# and the values are taken from the list of parameters.
|
||||||
|
return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)}
|
||||||
|
|
||||||
|
def get_optimizer(
|
||||||
|
self,
|
||||||
|
cpu_count: int,
|
||||||
|
random_state: int,
|
||||||
|
initial_points: int,
|
||||||
|
model_queue_size: int,
|
||||||
|
) -> Optimizer:
|
||||||
|
dimensions = self.dimensions
|
||||||
|
estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions)
|
||||||
|
|
||||||
|
acq_optimizer = "sampling"
|
||||||
|
if isinstance(estimator, str):
|
||||||
|
if estimator not in ("GP", "RF", "ET", "GBRT"):
|
||||||
|
raise OperationalException(f"Estimator {estimator} not supported.")
|
||||||
|
else:
|
||||||
|
acq_optimizer = "auto"
|
||||||
|
|
||||||
|
logger.info(f"Using estimator {estimator}.")
|
||||||
|
return Optimizer(
|
||||||
|
dimensions,
|
||||||
|
base_estimator=estimator,
|
||||||
|
acq_optimizer=acq_optimizer,
|
||||||
|
n_initial_points=initial_points,
|
||||||
|
acq_optimizer_kwargs={"n_jobs": cpu_count},
|
||||||
|
random_state=random_state,
|
||||||
|
model_queue_size=model_queue_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_params_details(self, params: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Return the params for each space
|
||||||
|
"""
|
||||||
|
result: dict = {}
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "buy"):
|
||||||
|
result["buy"] = {p.name: params.get(p.name) for p in self.buy_space}
|
||||||
|
if HyperoptTools.has_space(self.config, "sell"):
|
||||||
|
result["sell"] = {p.name: params.get(p.name) for p in self.sell_space}
|
||||||
|
if HyperoptTools.has_space(self.config, "protection"):
|
||||||
|
result["protection"] = {p.name: params.get(p.name) for p in self.protection_space}
|
||||||
|
if HyperoptTools.has_space(self.config, "roi"):
|
||||||
|
result["roi"] = {
|
||||||
|
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, "trailing"):
|
||||||
|
result["trailing"] = self.custom_hyperopt.generate_trailing_params(params)
|
||||||
|
if HyperoptTools.has_space(self.config, "trades"):
|
||||||
|
result["max_open_trades"] = {
|
||||||
|
"max_open_trades": (
|
||||||
|
self.backtesting.strategy.max_open_trades
|
||||||
|
if self.backtesting.strategy.max_open_trades != float("inf")
|
||||||
|
else -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_no_optimize_details(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get non-optimized parameters
|
||||||
|
"""
|
||||||
|
result: dict[str, Any] = {}
|
||||||
|
strategy = self.backtesting.strategy
|
||||||
|
if not HyperoptTools.has_space(self.config, "roi"):
|
||||||
|
result["roi"] = {str(k): v for k, v in strategy.minimal_roi.items()}
|
||||||
|
if not HyperoptTools.has_space(self.config, "stoploss"):
|
||||||
|
result["stoploss"] = {"stoploss": strategy.stoploss}
|
||||||
|
if not HyperoptTools.has_space(self.config, "trailing"):
|
||||||
|
result["trailing"] = {
|
||||||
|
"trailing_stop": strategy.trailing_stop,
|
||||||
|
"trailing_stop_positive": strategy.trailing_stop_positive,
|
||||||
|
"trailing_stop_positive_offset": strategy.trailing_stop_positive_offset,
|
||||||
|
"trailing_only_offset_is_reached": strategy.trailing_only_offset_is_reached,
|
||||||
|
}
|
||||||
|
if not HyperoptTools.has_space(self.config, "trades"):
|
||||||
|
result["max_open_trades"] = {"max_open_trades": strategy.max_open_trades}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def init_spaces(self):
|
||||||
|
"""
|
||||||
|
Assign the dimensions in the hyperoptimization space.
|
||||||
|
"""
|
||||||
|
if HyperoptTools.has_space(self.config, "protection"):
|
||||||
|
# Protections can only be optimized when using the Parameter interface
|
||||||
|
logger.debug("Hyperopt has 'protection' space")
|
||||||
|
# Enable Protections if protection space is selected.
|
||||||
|
self.config["enable_protections"] = True
|
||||||
|
self.backtesting.enable_protections = True
|
||||||
|
self.protection_space = self.custom_hyperopt.protection_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "buy"):
|
||||||
|
logger.debug("Hyperopt has 'buy' space")
|
||||||
|
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "sell"):
|
||||||
|
logger.debug("Hyperopt has 'sell' space")
|
||||||
|
self.sell_space = self.custom_hyperopt.sell_indicator_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "roi"):
|
||||||
|
logger.debug("Hyperopt has 'roi' space")
|
||||||
|
self.roi_space = self.custom_hyperopt.roi_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "stoploss"):
|
||||||
|
logger.debug("Hyperopt has 'stoploss' space")
|
||||||
|
self.stoploss_space = self.custom_hyperopt.stoploss_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "trailing"):
|
||||||
|
logger.debug("Hyperopt has 'trailing' space")
|
||||||
|
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "trades"):
|
||||||
|
logger.debug("Hyperopt has '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.roi_space
|
||||||
|
+ self.stoploss_space
|
||||||
|
+ self.trailing_space
|
||||||
|
+ self.max_open_trades_space
|
||||||
|
)
|
||||||
|
|
||||||
|
def assign_params(self, params_dict: dict[str, Any], category: str) -> None:
|
||||||
|
"""
|
||||||
|
Assign hyperoptable parameters
|
||||||
|
"""
|
||||||
|
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
||||||
|
if attr.optimize:
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
attr.value = params_dict[attr_name]
|
||||||
|
|
||||||
|
def generate_optimizer(self, raw_params: list[Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Used Optimize function.
|
||||||
|
Called once per epoch to optimize whatever is configured.
|
||||||
|
Keep this function as optimized as possible!
|
||||||
|
"""
|
||||||
|
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
|
||||||
|
backtest_start_time = datetime.now(timezone.utc)
|
||||||
|
params_dict = self._get_params_dict(self.dimensions, raw_params)
|
||||||
|
|
||||||
|
# Apply parameters
|
||||||
|
if HyperoptTools.has_space(self.config, "buy"):
|
||||||
|
self.assign_params(params_dict, "buy")
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "sell"):
|
||||||
|
self.assign_params(params_dict, "sell")
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "protection"):
|
||||||
|
self.assign_params(params_dict, "protection")
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "roi"):
|
||||||
|
self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(
|
||||||
|
params_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "stoploss"):
|
||||||
|
self.backtesting.strategy.stoploss = params_dict["stoploss"]
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "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"]
|
||||||
|
self.backtesting.strategy.trailing_stop_positive_offset = d[
|
||||||
|
"trailing_stop_positive_offset"
|
||||||
|
]
|
||||||
|
self.backtesting.strategy.trailing_only_offset_is_reached = d[
|
||||||
|
"trailing_only_offset_is_reached"
|
||||||
|
]
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, "trades"):
|
||||||
|
if self.config["stake_amount"] == "unlimited" and (
|
||||||
|
params_dict["max_open_trades"] == -1 or params_dict["max_open_trades"] == 0
|
||||||
|
):
|
||||||
|
# Ignore unlimited max open trades if stake amount is unlimited
|
||||||
|
params_dict.update({"max_open_trades": self.config["max_open_trades"]})
|
||||||
|
|
||||||
|
updated_max_open_trades = (
|
||||||
|
int(params_dict["max_open_trades"])
|
||||||
|
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.backtesting.strategy.max_open_trades = updated_max_open_trades
|
||||||
|
|
||||||
|
with self.data_pickle_file.open("rb") as f:
|
||||||
|
processed = load(f, mmap_mode="r")
|
||||||
|
if self.analyze_per_epoch:
|
||||||
|
# Data is not yet analyzed, rerun populate_indicators.
|
||||||
|
processed = self.advise_and_trim(processed)
|
||||||
|
|
||||||
|
bt_results = self.backtesting.backtest(
|
||||||
|
processed=processed, start_date=self.min_date, end_date=self.max_date
|
||||||
|
)
|
||||||
|
backtest_end_time = datetime.now(timezone.utc)
|
||||||
|
bt_results.update(
|
||||||
|
{
|
||||||
|
"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, params_dict, processed=processed
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_results_dict(
|
||||||
|
self,
|
||||||
|
backtesting_results: dict[str, Any],
|
||||||
|
min_date: datetime,
|
||||||
|
max_date: datetime,
|
||||||
|
params_dict: dict[str, Any],
|
||||||
|
processed: dict[str, DataFrame],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
params_details = self._get_params_details(params_dict)
|
||||||
|
|
||||||
|
strat_stats = generate_strategy_stats(
|
||||||
|
self.pairlist,
|
||||||
|
self.backtesting.strategy.get_strategy_name(),
|
||||||
|
backtesting_results,
|
||||||
|
min_date,
|
||||||
|
max_date,
|
||||||
|
market_change=self.market_change,
|
||||||
|
is_hyperopt=True,
|
||||||
|
)
|
||||||
|
results_explanation = HyperoptTools.format_results_explanation_string(
|
||||||
|
strat_stats, self.config["stake_currency"]
|
||||||
|
)
|
||||||
|
|
||||||
|
not_optimized = self.backtesting.strategy.get_no_optimize_params()
|
||||||
|
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
|
||||||
|
|
||||||
|
trade_count = strat_stats["total_trades"]
|
||||||
|
total_profit = strat_stats["profit_total"]
|
||||||
|
|
||||||
|
# If this evaluation contains too short amount of trades to be
|
||||||
|
# interesting -- consider it as 'bad' (assigned max. loss value)
|
||||||
|
# in order to cast this hyperspace point away from optimization
|
||||||
|
# path. We do not want to optimize 'hodl' strategies.
|
||||||
|
loss: float = MAX_LOSS
|
||||||
|
if trade_count >= self.config["hyperopt_min_trades"]:
|
||||||
|
loss = self.calculate_loss(
|
||||||
|
results=backtesting_results["results"],
|
||||||
|
trade_count=trade_count,
|
||||||
|
min_date=min_date,
|
||||||
|
max_date=max_date,
|
||||||
|
config=self.config,
|
||||||
|
processed=processed,
|
||||||
|
backtest_stats=strat_stats,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"loss": loss,
|
||||||
|
"params_dict": params_dict,
|
||||||
|
"params_details": params_details,
|
||||||
|
"params_not_optimized": not_optimized,
|
||||||
|
"results_metrics": strat_stats,
|
||||||
|
"results_explanation": results_explanation,
|
||||||
|
"total_profit": total_profit,
|
||||||
|
}
|
||||||
|
|
||||||
|
def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]:
|
||||||
|
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||||
|
|
||||||
|
# Trim startup period from analyzed dataframe to get correct dates for output.
|
||||||
|
# This is only used to keep track of min/max date after trimming.
|
||||||
|
# The result is NOT returned from this method, actual trimming happens in backtesting.
|
||||||
|
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
|
||||||
|
self.min_date, self.max_date = get_timerange(trimmed)
|
||||||
|
if not self.market_change:
|
||||||
|
self.market_change = calculate_market_change(trimmed, "close")
|
||||||
|
|
||||||
|
# Real trimming will happen as part of backtesting.
|
||||||
|
return preprocessed
|
||||||
|
|
||||||
|
def prepare_hyperopt_data(self) -> None:
|
||||||
|
HyperoptStateContainer.set_state(HyperoptState.DATALOAD)
|
||||||
|
data, self.timerange = self.backtesting.load_bt_data()
|
||||||
|
self.backtesting.load_bt_data_detail()
|
||||||
|
logger.info("Dataload complete. Calculating indicators")
|
||||||
|
|
||||||
|
if not self.analyze_per_epoch:
|
||||||
|
HyperoptStateContainer.set_state(HyperoptState.INDICATORS)
|
||||||
|
|
||||||
|
preprocessed = self.advise_and_trim(data)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Hyperopting with data from "
|
||||||
|
f"{self.min_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||||
|
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.
|
||||||
|
dump(preprocessed, self.data_pickle_file)
|
||||||
|
else:
|
||||||
|
dump(data, self.data_pickle_file)
|
||||||
Reference in New Issue
Block a user