From: Jérôme Benoit Date: Mon, 29 Dec 2025 14:47:29 +0000 (+0100) Subject: feat(quickadapter):add label_sampler option for optuna multi-objective HPO X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=cebd30442a77b7155e68de1ab184b433720cebc7;p=freqai-strategies.git feat(quickadapter):add label_sampler option for optuna multi-objective HPO Signed-off-by: Jérôme Benoit --- diff --git a/README.md b/README.md index dbae7aa..11138ad 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ docker compose up -d --build | freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier used for reversals labeling HPO. (Deprecated alias: `freqai.feature_parameters.max_label_natr_ratio`) | | freqai.feature_parameters.label_frequency_candles | `auto` | int >= 2 \| `auto` | Reversals labeling frequency. `auto` = max(2, 2 \* number of whitelisted pairs). | | freqai.feature_parameters.label_metric | `euclidean` | string (supported: `euclidean`,`minkowski`,`cityblock`,`chebyshev`,`mahalanobis`,`seuclidean`,`jensenshannon`,`sqeuclidean`,...) | Metric used in distance calculations to ideal point. | -| freqai.feature_parameters.label_weights | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights used in distance calculations to ideal point. Objectives: (1) number of detected reversals, (2) median swing amplitude, (3) median swing amplitude / median volatility-threshold ratio, (4) median swing volume per candle, (5) median swing speed, (6) median swing efficiency ratio, (7) median swing volume-weighted efficiency ratio. | +| freqai.feature_parameters.label_weights | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights used in distance calculations to ideal point. Objectives: (1) number of detected reversals, (2) median swing amplitude, (3) median (swing amplitude / median volatility-threshold ratio), (4) median swing volume per candle, (5) median swing speed, (6) median swing efficiency ratio, (7) median swing volume-weighted efficiency ratio. | | freqai.feature_parameters.label_p_order | `None` | float \| None | p-order used by `minkowski` / `power_mean` (optional). | | freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Metric used with `medoid`. | | freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Metric used for k-means clustering. | @@ -107,7 +107,8 @@ docker compose up -d --build | freqai.predictions_extrema.keep_extrema_fraction | 1.0 | float (0,1] | Fraction of extrema used for thresholds. `1.0` uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`. (Deprecated alias: `freqai.predictions_extrema.extrema_fraction`) | | _Optuna / HPO_ | | | | | freqai.optuna_hyperopt.enabled | false | bool | Enables HPO. | -| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate and group, `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). | +| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm for `hp` and `train` namespaces. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate and group, `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). | +| freqai.optuna_hyperopt.label_sampler | `auto` | enum {`auto`,`tpe`,`nsgaii`,`nsgaiii`} | HPO sampler algorithm for multi-objective `label` namespace. `nsgaii` uses [NSGAIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html), `nsgaiii` uses [NSGAIIISampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIIISampler.html). | | freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. | | freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. | | freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). | @@ -115,11 +116,11 @@ docker compose up -d --build | freqai.optuna_hyperopt.n_trials | 50 | int >= 1 | Maximum HPO trials. | | freqai.optuna_hyperopt.n_jobs | CPU threads / 4 | int >= 1 | Parallel HPO workers. | | freqai.optuna_hyperopt.timeout | 7200 | int >= 0 | HPO wall-clock timeout in seconds. | -| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR horizon search space. | -| freqai.optuna_hyperopt.train_candles_step | 10 | int >= 1 | Step for training sets size search space. | -| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable HPO search space reduction based on previous best parameters. | -| freqai.optuna_hyperopt.space_fraction | 0.4 | float [0,1] | Fraction of the HPO search space to use with `space_reduction`. Lower values create narrower search ranges around the best parameters. (Deprecated alias: `freqai.optuna_hyperopt.expansion_ratio`) | -| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per Hyperband pruner rung. | +| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR horizon `label` search space. | +| freqai.optuna_hyperopt.train_candles_step | 10 | int >= 1 | Step for training sets size `train` search space. | +| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable `hp` search space reduction based on previous best parameters. | +| freqai.optuna_hyperopt.space_fraction | 0.4 | float [0,1] | Fraction of the `hp` search space to use with `space_reduction`. Lower values create narrower search ranges around the best parameters. (Deprecated alias: `freqai.optuna_hyperopt.expansion_ratio`) | +| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per [HyperbandPruner](https://optuna.readthedocs.io/en/stable/reference/generated/optuna.pruners.HyperbandPruner.html) rung. | | freqai.optuna_hyperopt.seed | 1 | int >= 0 | HPO RNG seed. | ## ReforceXY diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 034d0df..95f1420 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -42,6 +42,7 @@ from Utils import ( ExtremaSelectionMethod = Literal["rank_extrema", "rank_peaks", "partition"] OptunaNamespace = Literal["hp", "train", "label"] +OptunaSampler = Literal["tpe", "auto", "nsgaii", "nsgaiii"] ClusterSelectionMethod = Literal["medoid", "min"] CustomThresholdMethod = Literal["median", "soft_extremum"] SkimageThresholdMethod = Literal[ @@ -71,7 +72,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): https://github.com/sponsors/robcaulk """ - version = "3.8.3" + version = "3.8.4" _TEST_SIZE: Final[float] = 0.1 @@ -111,7 +112,19 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) * _OPTUNA_LABEL_N_OBJECTIVES _OPTUNA_STORAGE_BACKENDS: Final[tuple[str, ...]] = ("file", "sqlite") - _OPTUNA_SAMPLERS: Final[tuple[str, ...]] = ("tpe", "auto") + _OPTUNA_HPO_SAMPLERS: Final[tuple[OptunaSampler, ...]] = ("tpe", "auto") + _OPTUNA_LABEL_SAMPLERS: Final[tuple[OptunaSampler, ...]] = ( + "auto", + "tpe", + "nsgaii", + "nsgaiii", + ) + _OPTUNA_SAMPLERS: Final[tuple[OptunaSampler, ...]] = ( + "tpe", + "auto", + "nsgaii", + "nsgaiii", + ) _OPTUNA_NAMESPACES: Final[tuple[OptunaNamespace, ...]] = ("hp", "train", "label") _SCIPY_METRICS: Final[tuple[str, ...]] = ( @@ -249,13 +262,16 @@ class QuickAdapterRegressorV3(BaseRegressionModel): .get("n_jobs", 1), max(int(self.max_system_threads / 4), 1), ), - "sampler": QuickAdapterRegressorV3._OPTUNA_SAMPLERS[0], # "tpe" + "sampler": QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS[0], # "tpe" "storage": QuickAdapterRegressorV3._OPTUNA_STORAGE_BACKENDS[0], # "file" "continuous": True, "warm_start": True, "n_startup_trials": 15, "n_trials": 50, "timeout": 7200, + "label_sampler": QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS[ + 0 + ], # "auto" "label_candles_step": 1, "train_candles_step": 10, "space_reduction": False, @@ -569,6 +585,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): if optuna_config.get("enabled"): logger.info(f" n_jobs: {optuna_config.get('n_jobs')}") logger.info(f" sampler: {optuna_config.get('sampler')}") + logger.info(f" label_sampler: {optuna_config.get('label_sampler')}") logger.info(f" storage: {optuna_config.get('storage')}") logger.info(f" continuous: {optuna_config.get('continuous')}") logger.info(f" warm_start: {optuna_config.get('warm_start')}") @@ -809,10 +826,26 @@ class QuickAdapterRegressorV3(BaseRegressionModel): f" keep_extrema_fraction: {format_number(predictions_extrema.get('keep_extrema_fraction'))}" ) + default_label_period_candles, default_label_natr_multiplier = ( + self._label_defaults + ) + label_period_candles = self.ft_params.get( + "label_period_candles", default_label_period_candles + ) + label_natr_multiplier = float( + self.ft_params.get("label_natr_multiplier", default_label_natr_multiplier) + ) logger.info("Label Configuration:") logger.info( f" fit_live_predictions_candles: {self.freqai_info.get('fit_live_predictions_candles', QuickAdapterRegressorV3.FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT)}" ) + if self._optuna_hyperopt: + logger.info( + f" label_period_candles: {label_period_candles} (initial value)" + ) + logger.info( + f" label_natr_multiplier: {format_number(label_natr_multiplier)} (initial value)" + ) logger.info(f" label_frequency_candles: {self._label_frequency_candles}") logger.info(f" min_label_period_candles: {self._min_label_period_candles}") logger.info(f" max_label_period_candles: {self._max_label_period_candles}") @@ -833,20 +866,17 @@ class QuickAdapterRegressorV3(BaseRegressionModel): f"label_natr_multiplier={format_number(params.get('label_natr_multiplier'))}" ) else: - default_label_period_candles, default_label_natr_multiplier = ( - self._label_defaults - ) logger.info("Label Parameters:") + logger.info(f" label_period_candles: {label_period_candles}") logger.info( - f" label_period_candles: {self.ft_params.get('label_period_candles', default_label_period_candles)}" - ) - logger.info( - f" label_natr_multiplier: {format_number(float(self.ft_params.get('label_natr_multiplier', default_label_natr_multiplier)))}" + f" label_natr_multiplier: {format_number(label_natr_multiplier)}" ) logger.info("=" * 60) - def get_optuna_params(self, pair: str, namespace: str) -> dict[str, Any]: + def get_optuna_params( + self, pair: str, namespace: OptunaNamespace + ) -> dict[str, Any]: if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0]: # "hp" params = self._optuna_hp_params.get(pair) elif namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[1]: # "train" @@ -861,7 +891,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return params def set_optuna_params( - self, pair: str, namespace: str, params: dict[str, Any] + self, pair: str, namespace: OptunaNamespace, params: dict[str, Any] ) -> None: if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0]: # "hp" self._optuna_hp_params[pair] = params @@ -875,7 +905,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES)}" ) - def get_optuna_value(self, pair: str, namespace: str) -> float: + def get_optuna_value(self, pair: str, namespace: OptunaNamespace) -> float: if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0]: # "hp" value = self._optuna_hp_value.get(pair) elif namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[1]: # "train" @@ -887,7 +917,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) return value - def set_optuna_value(self, pair: str, namespace: str, value: float) -> None: + def set_optuna_value( + self, pair: str, namespace: OptunaNamespace, value: float + ) -> None: if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0]: # "hp" self._optuna_hp_value[pair] = value elif namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[1]: # "train" @@ -898,7 +930,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel): f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES[:2])}" # Only hp and train ) - def get_optuna_values(self, pair: str, namespace: str) -> list[float | int]: + def get_optuna_values( + self, pair: str, namespace: OptunaNamespace + ) -> list[float | int]: if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]: # "label" values = self._optuna_label_values.get(pair) else: @@ -909,7 +943,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return values def set_optuna_values( - self, pair: str, namespace: str, values: list[float | int] + self, pair: str, namespace: OptunaNamespace, values: list[float | int] ) -> None: if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]: # "label" self._optuna_label_values[pair] = values @@ -1112,7 +1146,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): def optuna_throttle_callback( self, pair: str, - namespace: str, + namespace: OptunaNamespace, callback: Callable[[], Optional[optuna.study.Study]], ) -> None: if namespace not in { @@ -2163,7 +2197,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) def _get_multi_objective_study_best_trial( - self, namespace: str, study: optuna.study.Study + self, namespace: OptunaNamespace, study: optuna.study.Study ) -> Optional[optuna.trial.FrozenTrial]: if namespace not in { QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2] @@ -2221,7 +2255,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): def optuna_optimize( self, pair: str, - namespace: str, + namespace: OptunaNamespace, objective: ObjectiveFuncType, direction: Optional[optuna.study.StudyDirection] = None, directions: Optional[list[optuna.study.StudyDirection]] = None, @@ -2371,31 +2405,68 @@ class QuickAdapterRegressorV3(BaseRegressionModel): else: return optuna.pruners.NopPruner() - def optuna_create_sampler(self) -> optuna.samplers.BaseSampler: - sampler = self._optuna_config.get( - "sampler", QuickAdapterRegressorV3._OPTUNA_SAMPLERS[0] - ) - if sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[1]: # "auto" - return optunahub.load_module("samplers/auto_sampler").AutoSampler( - seed=self._optuna_config.get("seed") + def optuna_create_sampler( + self, sampler: Optional[OptunaSampler] = None + ) -> optuna.samplers.BaseSampler: + if sampler is None: + sampler = self._optuna_config.get( + "sampler", ) - elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[0]: # "tpe" + if sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[0]: # "tpe" return optuna.samplers.TPESampler( n_startup_trials=self._optuna_config.get("n_startup_trials"), multivariate=True, group=True, seed=self._optuna_config.get("seed"), ) + elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[1]: # "auto" + return optunahub.load_module("samplers/auto_sampler").AutoSampler( + seed=self._optuna_config.get("seed") + ) + elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[2]: # "nsgaii" + return optuna.samplers.NSGAIISampler( + seed=self._optuna_config.get("seed"), + ) + elif sampler == QuickAdapterRegressorV3._OPTUNA_SAMPLERS[3]: # "nsgaiii" + return optuna.samplers.NSGAIIISampler( + seed=self._optuna_config.get("seed"), + ) else: raise ValueError( f"Invalid optuna sampler {sampler!r}. " f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_SAMPLERS)}" ) + def optuna_samplers_by_namespace( + self, namespace: OptunaNamespace + ) -> tuple[tuple[OptunaSampler, ...], OptunaSampler]: + if namespace in { + QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0], # "hp" + QuickAdapterRegressorV3._OPTUNA_NAMESPACES[1], # "train" + }: + return ( + QuickAdapterRegressorV3._OPTUNA_HPO_SAMPLERS, + self._optuna_config.get( + "sampler", + ), + ) + elif namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]: # "label" + return ( + QuickAdapterRegressorV3._OPTUNA_LABEL_SAMPLERS, + self._optuna_config.get( + "label_sampler", + ), + ) + else: + raise ValueError( + f"Invalid namespace {namespace!r}. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES)}" + ) + def optuna_create_study( self, pair: str, - namespace: str, + namespace: OptunaNamespace, direction: Optional[optuna.study.StudyDirection] = None, directions: Optional[list[optuna.study.StudyDirection]] = None, ) -> Optional[optuna.study.Study]: @@ -2429,10 +2500,17 @@ class QuickAdapterRegressorV3(BaseRegressionModel): pair, namespace, study_name, storage ) + samplers, sampler = self.optuna_samplers_by_namespace(namespace) + if sampler not in set(samplers): + raise ValueError( + f"Invalid optuna {namespace} sampler {sampler!r}. " + f"Supported: {', '.join(samplers)}" + ) + try: return optuna.create_study( study_name=study_name, - sampler=self.optuna_create_sampler(), + sampler=self.optuna_create_sampler(sampler), pruner=self.optuna_create_pruner(is_study_single_objective), direction=direction, directions=directions, @@ -2447,7 +2525,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return None def optuna_validate_params( - self, pair: str, namespace: str, study: Optional[optuna.study.Study] + self, pair: str, namespace: OptunaNamespace, study: Optional[optuna.study.Study] ) -> bool: if not study: return False @@ -2467,7 +2545,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return self.optuna_validate_value(best_value) is not None def optuna_enqueue_previous_best_params( - self, pair: str, namespace: str, study: Optional[optuna.study.Study] + self, pair: str, namespace: OptunaNamespace, study: Optional[optuna.study.Study] ) -> None: if not study: return @@ -2485,7 +2563,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): exc_info=True, ) - def optuna_save_best_params(self, pair: str, namespace: str) -> None: + def optuna_save_best_params(self, pair: str, namespace: OptunaNamespace) -> None: best_params_path = Path( self.full_path / f"optuna-{namespace}-best-params-{pair.split('/')[0]}.json" ) @@ -2500,7 +2578,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): raise def optuna_load_best_params( - self, pair: str, namespace: str + self, pair: str, namespace: OptunaNamespace ) -> Optional[dict[str, Any]]: best_params_path = Path( self.full_path / f"optuna-{namespace}-best-params-{pair.split('/')[0]}.json" @@ -2512,7 +2590,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): @staticmethod def optuna_delete_study( - pair: str, namespace: str, study_name: str, storage: optuna.storages.BaseStorage + pair: str, + namespace: OptunaNamespace, + study_name: str, + storage: optuna.storages.BaseStorage, ) -> None: try: optuna.delete_study(study_name=study_name, storage=storage) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 63f053b..00fa93c 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -106,7 +106,7 @@ class QuickAdapterV3(IStrategy): _TRADING_MODES: Final[tuple[TradingMode, ...]] = ("spot", "margin", "futures") def version(self) -> str: - return "3.8.3" + return "3.8.4" timeframe = "5m" diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index af9c6f3..e38a667 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -2101,7 +2101,6 @@ def fit_regressor( scoring="neg_root_mean_squared_error", **model_training_parameters, ) - model.fit( X=X, y=y.to_numpy().ravel(),