change optimizer to optuna

This commit is contained in:
viotemp1
2025-03-25 14:06:35 +02:00
parent 31e4501765
commit 62f05964b4
5 changed files with 119 additions and 53 deletions

View File

@@ -37,7 +37,7 @@ INITIAL_POINTS = 30
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt model queue, to optimize memory consumption
SKOPT_MODEL_QUEUE_SIZE = 10
# SKOPT_MODEL_QUEUE_SIZE = 10
log_queue: Any
@@ -165,7 +165,13 @@ class Hyperopt:
def _set_random_state(self, random_state: int | None) -> int:
return random_state or random.randint(1, 2**16 - 1) # noqa: S311
def get_asked_points(self, n_points: int) -> tuple[list[list[Any]], list[bool]]:
def get_optuna_asked_points(self, n_points: int, dimensions: dict) -> list[Any]:
asked: list[list[Any]] = []
for i in range(n_points):
asked.append(self.opt.ask(dimensions))
return asked
def get_asked_points(self, n_points: int, dimensions: dict) -> tuple[list[list[Any]], list[bool]]:
"""
Enforce points returned from `self.opt.ask` have not been already evaluated
@@ -191,7 +197,8 @@ class Hyperopt:
while i < 5 and len(asked_non_tried) < n_points:
if i < 3:
self.opt.cache_ = {}
asked = unique_list(self.opt.ask(n_points=n_points * 5 if i > 0 else n_points))
asked = unique_list(self.get_optuna_asked_points(n_points=n_points * 5 if i > 0 else n_points,
dimensions=dimensions))
is_random = [False for _ in range(len(asked))]
else:
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
@@ -199,10 +206,10 @@ class Hyperopt:
is_random_non_tried += [
rand
for x, rand in zip(asked, is_random, strict=False)
if x not in self.opt.Xi and x not in asked_non_tried
if 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
x for x in asked if x not in asked_non_tried
]
i += 1
@@ -212,7 +219,7 @@ class Hyperopt:
is_random_non_tried[: min(len(asked_non_tried), n_points)],
)
else:
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
return self.get_optuna_asked_points(n_points=n_points, dimensions=dimensions), [False for _ in range(n_points)]
def evaluate_result(self, val: dict[str, Any], current: int, is_random: bool):
"""
@@ -258,9 +265,7 @@ class Hyperopt:
config_jobs = self.config.get("hyperopt_jobs", -1)
logger.info(f"Number of parallel jobs set as: {config_jobs}")
self.opt = self.hyperopter.get_optimizer(
config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE
)
self.opt = self.hyperopter.get_optimizer(self.random_state)
self._setup_logging_mp_workaround()
try:
with Parallel(n_jobs=config_jobs) as parallel:
@@ -276,9 +281,9 @@ class Hyperopt:
if self.analyze_per_epoch:
# First analysis not in parallel mode when using --analyze-per-epoch.
# This allows dataprovider to load it's informative cache.
asked, is_random = self.get_asked_points(n_points=1)
f_val0 = self.hyperopter.generate_optimizer(asked[0])
self.opt.tell(asked, [f_val0["loss"]])
asked, is_random = self.get_asked_points(n_points=1, dimensions=self.hyperopter.o_dimensions)
f_val0 = self.hyperopter.generate_optimizer(asked[0].params)
self.opt.tell(asked[0], [f_val0["loss"]])
self.evaluate_result(f_val0, 1, is_random[0])
pbar.update(task, advance=1)
start += 1
@@ -290,9 +295,12 @@ class Hyperopt:
n_rest = (i + 1) * jobs - (self.total_epochs - start)
current_jobs = jobs - n_rest if n_rest > 0 else jobs
asked, is_random = self.get_asked_points(n_points=current_jobs)
f_val = self.run_optimizer_parallel(parallel, asked)
self.opt.tell(asked, [v["loss"] for v in f_val])
asked, is_random = self.get_asked_points(
n_points=current_jobs, dimensions=self.hyperopter.o_dimensions)
f_val = self.run_optimizer_parallel(parallel, [asked1.params for asked1 in asked])
for o_ask, v in zip(asked, f_val):
self.opt.tell(o_ask, v["loss"])
# self.opt.tell(asked, [v["loss"] for v in f_val])
for j, val in enumerate(f_val):
# Use human-friendly indexes here (starting from 1)

View File

@@ -44,11 +44,12 @@ class IHyperOpt(ABC):
def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType:
"""
Return base_estimator.
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
inheriting from RegressorMixin (from sklearn).
Can be any of "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIISampler"
"NSGAIIISampler", "QMCSampler" or an instance of a class
inheriting from BaseSampler (from optuna.samplers).
"""
return "ET"
return "NSGAIISampler"
def generate_roi_table(self, params: dict) -> dict[int, float]:
"""
Create a ROI table.

View File

@@ -30,11 +30,14 @@ from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
# Suppress scikit-learn FutureWarnings from skopt
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
from skopt import Optimizer
# from skopt import Optimizer
from freqtrade.optimize.space.decimalspace import SKDecimal
from skopt.space import Categorical, Integer, Real
import optuna
import optunahub
from skopt.space import Dimension
logger = logging.getLogger(__name__)
@@ -58,6 +61,7 @@ class HyperOptimizer:
self.trailing_space: list[Dimension] = []
self.max_open_trades_space: list[Dimension] = []
self.dimensions: list[Dimension] = []
self.o_dimensions: Dict = {}
self.config = config
self.min_date: datetime
@@ -127,8 +131,9 @@ class HyperOptimizer:
self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict(
self, dimensions: list[Dimension], raw_params: list[Any]
self, dimensions: list[Dimension], raw_params: dict[str, Any] # list[Any]
) -> dict[str, Any]:
# logger.info(f"_get_params_dict: {raw_params}")
# Ensure the number of dimensions match
# the number of parameters in the list.
if len(raw_params) != len(dimensions):
@@ -136,7 +141,10 @@ class HyperOptimizer:
# 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)}
# result = {d.name: v for d, v in zip(dimensions, raw_params, strict=False)}
# logger.info(f"d_get_params_dict: {result}")
# return {d.name: v for d, v in zip(dimensions, raw_params.params, strict=False)}
return raw_params
def _get_params_details(self, params: dict) -> dict:
"""
@@ -377,33 +385,78 @@ class HyperOptimizer:
"total_profit": total_profit,
}
def convert_dimensions_to_optuna_space(self, s_dimensions: list[Dimension]) -> dict:
o_dimensions = {}
for original_dim in s_dimensions:
if type(original_dim) == Integer: # isinstance(original_dim, Integer):
o_dimensions[original_dim.name] = optuna.distributions.IntDistribution(
original_dim.low, original_dim.high, log=False, step=1
)
elif (
type(original_dim) == SKDecimal
):
o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution(
original_dim.low_orig,
original_dim.high_orig,
log=False,
step=1 / pow(10, original_dim.decimals)
)
elif (
type(original_dim) == Real
):
o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution(
original_dim.low,
original_dim.high,
log=False,
)
elif (
type(original_dim) == Categorical
):
o_dimensions[original_dim.name] = optuna.distributions.CategoricalDistribution(
list(original_dim.bounds)
)
else:
raise Exception(
f"Unknown search space {original_dim} / {type(original_dim)}"
)
# logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}")
return o_dimensions
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"
o_sampler = self.custom_hyperopt.generate_estimator(dimensions=self.dimensions)
self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions)
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,
)
# for save/restore
# with open("sampler.pkl", "wb") as fout:
# pickle.dump(study.sampler, fout)
# restored_sampler = pickle.load(open("sampler.pkl", "rb"))
if isinstance(o_sampler, str):
if o_sampler not in ("TPESampler", "GPSampler", "CmaEsSampler",
"NSGAIISampler", "NSGAIIISampler", "QMCSampler"
):
raise OperationalException(f"Optuna Sampler {o_sampler} not supported.")
if o_sampler == "TPESampler":
sampler = optuna.samplers.TPESampler(seed=random_state)
elif o_sampler == "GPSampler":
sampler = optuna.samplers.GPSampler(seed=random_state)
elif o_sampler == "CmaEsSampler":
sampler = optuna.samplers.CmaEsSampler(seed=random_state)
elif o_sampler == "NSGAIISampler":
sampler = optuna.samplers.NSGAIISampler(seed=random_state)
elif o_sampler == "NSGAIIISampler":
sampler = optuna.samplers.NSGAIIISampler(seed=random_state)
elif o_sampler == "QMCSampler":
sampler = optuna.samplers.QMCSampler(seed=random_state)
logger.info(f"Using optuna sampler {o_sampler}.")
return optuna.create_study(sampler=sampler, direction="minimize")
def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]:
preprocessed = self.backtesting.strategy.advise_all_indicators(data)

View File

@@ -6,3 +6,6 @@ scipy==1.15.2
scikit-learn==1.6.1
ft-scikit-optimize==0.9.2
filelock==3.18.0
optuna==4.2.1
optunahub==0.2.0
cmaes==0.11.1

View File

@@ -606,9 +606,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10)
hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13)
hyperopt.hyperopter.init_spaces()
generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(
list(optimizer_param.values())
)
generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param)
# list(optimizer_param.values())
assert generate_optimizer_value == response_expected
@@ -1088,8 +1088,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
assert opt.backtesting.strategy.max_open_trades != 1
opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1"
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
opt.get_optimizer(2, 42, 2, 2)
with pytest.raises(OperationalException, match="Optuna Sampler ET1 not supported."):
opt.get_optimizer(42)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@@ -1313,9 +1313,10 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal first_time_evaluated
print(f"max_open_trades: {hyperopt.hyperopter.backtesting.strategy.max_open_trades}")
stake_amount = func(*args, **kwargs)
if first_time_evaluated is False:
assert stake_amount == 1
assert stake_amount == 2
first_time_evaluated = True
return stake_amount
@@ -1329,5 +1330,5 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
hyperopt.start()
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 8
assert hyperopt.config["max_open_trades"] == 8
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 4
assert hyperopt.config["max_open_trades"] == 4