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
|
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||||
# 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
|
||||||
|
|
||||||
log_queue: Any
|
log_queue: Any
|
||||||
|
|
||||||
@@ -165,7 +165,13 @@ class Hyperopt:
|
|||||||
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 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
|
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:
|
while i < 5 and len(asked_non_tried) < n_points:
|
||||||
if i < 3:
|
if i < 3:
|
||||||
self.opt.cache_ = {}
|
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))]
|
is_random = [False for _ in range(len(asked))]
|
||||||
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))
|
||||||
@@ -199,10 +206,10 @@ class Hyperopt:
|
|||||||
is_random_non_tried += [
|
is_random_non_tried += [
|
||||||
rand
|
rand
|
||||||
for x, rand in zip(asked, is_random, strict=False)
|
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 += [
|
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
|
i += 1
|
||||||
|
|
||||||
@@ -212,7 +219,7 @@ class Hyperopt:
|
|||||||
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.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):
|
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)
|
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.hyperopter.get_optimizer(
|
self.opt = self.hyperopter.get_optimizer(self.random_state)
|
||||||
config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE
|
|
||||||
)
|
|
||||||
self._setup_logging_mp_workaround()
|
self._setup_logging_mp_workaround()
|
||||||
try:
|
try:
|
||||||
with Parallel(n_jobs=config_jobs) as parallel:
|
with Parallel(n_jobs=config_jobs) as parallel:
|
||||||
@@ -276,9 +281,9 @@ class Hyperopt:
|
|||||||
if self.analyze_per_epoch:
|
if self.analyze_per_epoch:
|
||||||
# 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, dimensions=self.hyperopter.o_dimensions)
|
||||||
f_val0 = self.hyperopter.generate_optimizer(asked[0])
|
f_val0 = self.hyperopter.generate_optimizer(asked[0].params)
|
||||||
self.opt.tell(asked, [f_val0["loss"]])
|
self.opt.tell(asked[0], [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
|
||||||
@@ -290,9 +295,12 @@ class Hyperopt:
|
|||||||
n_rest = (i + 1) * jobs - (self.total_epochs - start)
|
n_rest = (i + 1) * jobs - (self.total_epochs - start)
|
||||||
current_jobs = jobs - n_rest if n_rest > 0 else jobs
|
current_jobs = jobs - n_rest if n_rest > 0 else jobs
|
||||||
|
|
||||||
asked, is_random = self.get_asked_points(n_points=current_jobs)
|
asked, is_random = self.get_asked_points(
|
||||||
f_val = self.run_optimizer_parallel(parallel, asked)
|
n_points=current_jobs, dimensions=self.hyperopter.o_dimensions)
|
||||||
self.opt.tell(asked, [v["loss"] for v in f_val])
|
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):
|
for j, val in enumerate(f_val):
|
||||||
# Use human-friendly indexes here (starting from 1)
|
# Use human-friendly indexes here (starting from 1)
|
||||||
|
|||||||
@@ -44,11 +44,12 @@ class IHyperOpt(ABC):
|
|||||||
def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType:
|
def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType:
|
||||||
"""
|
"""
|
||||||
Return base_estimator.
|
Return base_estimator.
|
||||||
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
|
Can be any of "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIISampler"
|
||||||
inheriting from RegressorMixin (from sklearn).
|
"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]:
|
def generate_roi_table(self, params: dict) -> dict[int, float]:
|
||||||
"""
|
"""
|
||||||
Create a ROI table.
|
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.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
|
||||||
|
|
||||||
|
|
||||||
# Suppress scikit-learn FutureWarnings from skopt
|
# Suppress scikit-learn FutureWarnings from skopt
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
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
|
from skopt.space import Dimension
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -58,6 +61,7 @@ class HyperOptimizer:
|
|||||||
self.trailing_space: list[Dimension] = []
|
self.trailing_space: list[Dimension] = []
|
||||||
self.max_open_trades_space: list[Dimension] = []
|
self.max_open_trades_space: list[Dimension] = []
|
||||||
self.dimensions: list[Dimension] = []
|
self.dimensions: list[Dimension] = []
|
||||||
|
self.o_dimensions: Dict = {}
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.min_date: datetime
|
self.min_date: datetime
|
||||||
@@ -127,8 +131,9 @@ class HyperOptimizer:
|
|||||||
self.hyperopt_pickle_magic(modules.__bases__)
|
self.hyperopt_pickle_magic(modules.__bases__)
|
||||||
|
|
||||||
def _get_params_dict(
|
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]:
|
) -> dict[str, Any]:
|
||||||
|
# logger.info(f"_get_params_dict: {raw_params}")
|
||||||
# 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):
|
||||||
@@ -136,7 +141,10 @@ class HyperOptimizer:
|
|||||||
|
|
||||||
# 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.
|
||||||
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:
|
def _get_params_details(self, params: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -377,33 +385,78 @@ class HyperOptimizer:
|
|||||||
"total_profit": total_profit,
|
"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(
|
def get_optimizer(
|
||||||
self,
|
self,
|
||||||
cpu_count: int,
|
|
||||||
random_state: 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"
|
o_sampler = self.custom_hyperopt.generate_estimator(dimensions=self.dimensions)
|
||||||
if isinstance(estimator, str):
|
self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions)
|
||||||
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}.")
|
# for save/restore
|
||||||
return Optimizer(
|
# with open("sampler.pkl", "wb") as fout:
|
||||||
dimensions,
|
# pickle.dump(study.sampler, fout)
|
||||||
base_estimator=estimator,
|
# restored_sampler = pickle.load(open("sampler.pkl", "rb"))
|
||||||
acq_optimizer=acq_optimizer,
|
|
||||||
n_initial_points=initial_points,
|
if isinstance(o_sampler, str):
|
||||||
acq_optimizer_kwargs={"n_jobs": cpu_count},
|
if o_sampler not in ("TPESampler", "GPSampler", "CmaEsSampler",
|
||||||
random_state=random_state,
|
"NSGAIISampler", "NSGAIIISampler", "QMCSampler"
|
||||||
model_queue_size=model_queue_size,
|
):
|
||||||
)
|
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]:
|
def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]:
|
||||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ scipy==1.15.2
|
|||||||
scikit-learn==1.6.1
|
scikit-learn==1.6.1
|
||||||
ft-scikit-optimize==0.9.2
|
ft-scikit-optimize==0.9.2
|
||||||
filelock==3.18.0
|
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.min_date = dt_utc(2017, 12, 10)
|
||||||
hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13)
|
hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13)
|
||||||
hyperopt.hyperopter.init_spaces()
|
hyperopt.hyperopter.init_spaces()
|
||||||
generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(
|
generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param)
|
||||||
list(optimizer_param.values())
|
# list(optimizer_param.values())
|
||||||
)
|
|
||||||
assert generate_optimizer_value == response_expected
|
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
|
assert opt.backtesting.strategy.max_open_trades != 1
|
||||||
|
|
||||||
opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1"
|
opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1"
|
||||||
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
|
with pytest.raises(OperationalException, match="Optuna Sampler ET1 not supported."):
|
||||||
opt.get_optimizer(2, 42, 2, 2)
|
opt.get_optimizer(42)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||||
@@ -1313,9 +1313,10 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
nonlocal first_time_evaluated
|
nonlocal first_time_evaluated
|
||||||
|
print(f"max_open_trades: {hyperopt.hyperopter.backtesting.strategy.max_open_trades}")
|
||||||
stake_amount = func(*args, **kwargs)
|
stake_amount = func(*args, **kwargs)
|
||||||
if first_time_evaluated is False:
|
if first_time_evaluated is False:
|
||||||
assert stake_amount == 1
|
assert stake_amount == 2
|
||||||
first_time_evaluated = True
|
first_time_evaluated = True
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
|
||||||
@@ -1329,5 +1330,5 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
|
|||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
||||||
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 8
|
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 4
|
||||||
assert hyperopt.config["max_open_trades"] == 8
|
assert hyperopt.config["max_open_trades"] == 4
|
||||||
|
|||||||
Reference in New Issue
Block a user