mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 00:23:07 +00:00
change optimizer to optuna
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user