From: Jérôme Benoit Date: Thu, 20 Nov 2025 21:41:32 +0000 (+0100) Subject: refactor: cleanup tunables handling X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=ad2b4aa011988c503dcb2ecb8ee451adb38b06e7;p=freqai-strategies.git refactor: cleanup tunables handling Signed-off-by: Jérôme Benoit --- diff --git a/README.md b/README.md index 07b0f2a..2604725 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ docker compose up -d --build | freqai.feature_parameters.label_knn_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. | | _Predictions extrema_ | | | | | freqai.predictions_extrema.selection_method | `extrema_rank` | enum {`peak_values`,`extrema_rank`,`partition`} | Extrema selection method. `peak_values` uses detected peaks, `extrema_rank` uses ranked extrema values, `partition` uses sign-based extrema partitioning. | -| freqai.predictions_extrema.thresholds_smoothing | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. | +| freqai.predictions_extrema.thresholds_smoothing | `mean` | enum {`mean`,`median`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. | | freqai.predictions_extrema.thresholds_alpha | 12.0 | float > 0 | Alpha for `soft_extremum`. | | freqai.predictions_extrema.threshold_outlier | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. | | _Optuna / HPO_ | | | | diff --git a/ReforceXY/user_data/freqaimodels/ReforceXY.py b/ReforceXY/user_data/freqaimodels/ReforceXY.py index 8151564..3058e21 100644 --- a/ReforceXY/user_data/freqaimodels/ReforceXY.py +++ b/ReforceXY/user_data/freqaimodels/ReforceXY.py @@ -73,8 +73,8 @@ from stable_baselines3.common.vec_env import ( ) ModelType = Literal["PPO", "RecurrentPPO", "MaskablePPO", "DQN", "QRDQN"] -ScheduleType = Literal["linear", "constant", "unknown"] ScheduleTypeKnown = Literal["linear", "constant"] +ScheduleType = Union[ScheduleTypeKnown, Literal["unknown"]] ExitPotentialMode = Literal[ "canonical", "non_canonical", @@ -85,7 +85,8 @@ ExitPotentialMode = Literal[ TransformFunction = Literal["tanh", "softsign", "arctan", "sigmoid", "asinh", "clip"] ExitAttenuationMode = Literal["legacy", "sqrt", "linear", "power", "half_life"] ActivationFunction = Literal["tanh", "relu", "elu", "leaky_relu"] -OptimizerClass = Literal["adam", "adamw", "rmsprop"] +OptimizerClassOptuna = Literal["adamw", "rmsprop"] +OptimizerClass = Union[OptimizerClassOptuna, Literal["adam"]] NetArchSize = Literal["small", "medium", "large", "extra_large"] StorageBackend = Literal["sqlite", "file"] SamplerType = Literal["tpe", "auto"] @@ -156,8 +157,11 @@ class ReforceXY(BaseReinforcementLearningModel): "DQN", "QRDQN", ) - _SCHEDULE_TYPES: Final[tuple[ScheduleType, ...]] = ("linear", "constant", "unknown") _SCHEDULE_TYPES_KNOWN: Final[tuple[ScheduleTypeKnown, ...]] = ("linear", "constant") + _SCHEDULE_TYPES: Final[tuple[ScheduleType, ...]] = ( + *_SCHEDULE_TYPES_KNOWN, + "unknown", + ) _EXIT_POTENTIAL_MODES: Final[tuple[ExitPotentialMode, ...]] = ( "canonical", "non_canonical", @@ -186,8 +190,14 @@ class ReforceXY(BaseReinforcementLearningModel): "elu", "leaky_relu", ) - _OPTIMIZER_CLASSES: Final[tuple[OptimizerClass, ...]] = ("adam", "adamw", "rmsprop") - _OPTIMIZER_CLASSES_OPTUNA: Final[tuple[OptimizerClass, ...]] = ("adamw", "rmsprop") + _OPTIMIZER_CLASSES_OPTUNA: Final[tuple[OptimizerClassOptuna, ...]] = ( + "adamw", + "rmsprop", + ) + _OPTIMIZER_CLASSES: Final[tuple[OptimizerClass, ...]] = ( + *_OPTIMIZER_CLASSES_OPTUNA, + "adam", + ) _NET_ARCH_SIZES: Final[tuple[NetArchSize, ...]] = ( "small", "medium", @@ -604,7 +614,7 @@ class ReforceXY(BaseReinforcementLearningModel): ) model_params["policy_kwargs"]["optimizer_class"] = get_optimizer_class( model_params.get("policy_kwargs", {}).get( - "optimizer_class", ReforceXY._OPTIMIZER_CLASSES[1] + "optimizer_class", ReforceXY._OPTIMIZER_CLASSES[0] ) # "adamw" ) @@ -3842,9 +3852,9 @@ def get_optimizer_class( Get optimizer class """ return { - ReforceXY._OPTIMIZER_CLASSES[0]: th.optim.Adam, # "adam" - ReforceXY._OPTIMIZER_CLASSES[1]: th.optim.AdamW, # "adamw" - ReforceXY._OPTIMIZER_CLASSES[2]: th.optim.RMSprop, # "rmsprop" + ReforceXY._OPTIMIZER_CLASSES[0]: th.optim.AdamW, # "adamw" + ReforceXY._OPTIMIZER_CLASSES[1]: th.optim.RMSprop, # "rmsprop" + ReforceXY._OPTIMIZER_CLASSES[2]: th.optim.Adam, # "adam" }.get(optimizer_class_name, th.optim.Adam) diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 2f45f92..007d580 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -6,7 +6,7 @@ import time import warnings from functools import cached_property from pathlib import Path -from typing import Any, Callable, Final, Literal, Optional +from typing import Any, Callable, Final, Literal, Optional, Union import numpy as np import optuna @@ -35,6 +35,11 @@ from Utils import ( ExtremaSelectionMethod = Literal["peak_values", "extrema_rank", "partition"] OptunaNamespace = Literal["hp", "train", "label"] +CustomThresholdMethod = Literal["median", "soft_extremum"] +SkimageThresholdMethod = Literal[ + "isodata", "li", "mean", "minimum", "otsu", "triangle", "yen" +] +ThresholdMethod = Union[CustomThresholdMethod, SkimageThresholdMethod] debug = False @@ -75,6 +80,23 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "extrema_rank", "partition", ) + _CUSTOM_THRESHOLD_METHODS: Final[tuple[CustomThresholdMethod, ...]] = ( + "median", + "soft_extremum", + ) + _SKIMAGE_THRESHOLD_METHODS: Final[tuple[SkimageThresholdMethod, ...]] = ( + "isodata", + "li", + "mean", + "minimum", + "otsu", + "triangle", + "yen", + ) + _THRESHOLD_METHODS: Final[tuple[ThresholdMethod, ...]] = ( + *_CUSTOM_THRESHOLD_METHODS, + *_SKIMAGE_THRESHOLD_METHODS, + ) _OPTUNA_STORAGE_BACKENDS: Final[tuple[str, ...]] = ("sqlite", "file") _OPTUNA_SAMPLERS: Final[tuple[str, ...]] = ("tpe", "auto") _OPTUNA_NAMESPACES: Final[tuple[OptunaNamespace, ...]] = ("hp", "train", "label") @@ -83,6 +105,18 @@ class QuickAdapterRegressorV3(BaseRegressionModel): def _extrema_selection_methods_set() -> set[ExtremaSelectionMethod]: return set(QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS) + @staticmethod + def _custom_threshold_methods_set() -> set[CustomThresholdMethod]: + return set(QuickAdapterRegressorV3._CUSTOM_THRESHOLD_METHODS) + + @staticmethod + def _skimage_threshold_methods_set() -> set[SkimageThresholdMethod]: + return set(QuickAdapterRegressorV3._SKIMAGE_THRESHOLD_METHODS) + + @staticmethod + def _threshold_methods_set() -> set[ThresholdMethod]: + return set(QuickAdapterRegressorV3._THRESHOLD_METHODS) + @staticmethod def _optuna_namespaces_set() -> set[OptunaNamespace]: return set(QuickAdapterRegressorV3._OPTUNA_NAMESPACES) @@ -728,39 +762,42 @@ class QuickAdapterRegressorV3(BaseRegressionModel): QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS[1], ) ) - if extrema_selection not in self._extrema_selection_methods_set(): + if ( + extrema_selection + not in QuickAdapterRegressorV3._extrema_selection_methods_set() + ): raise ValueError( f"Unsupported extrema selection method: {extrema_selection}. " - f"Supported methods are {', '.join(self._EXTREMA_SELECTION_METHODS)}" + f"Supported methods are {', '.join(QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS)}" ) thresholds_smoothing = str( - predictions_extrema.get("thresholds_smoothing", "mean") - ) - skimage_thresholds_smoothing_methods = { - "isodata", - "li", - "mean", - "minimum", - "otsu", - "triangle", - "yen", - } - thresholds_smoothing_methods = skimage_thresholds_smoothing_methods.union( - {"soft_extremum"} + predictions_extrema.get( + "thresholds_smoothing", + QuickAdapterRegressorV3._SKIMAGE_THRESHOLD_METHODS[2], + ) ) - if thresholds_smoothing == "soft_extremum": + if thresholds_smoothing not in QuickAdapterRegressorV3._threshold_methods_set(): + raise ValueError( + f"Unsupported thresholds smoothing method: {thresholds_smoothing}. " + f"Supported methods are {', '.join(QuickAdapterRegressorV3._THRESHOLD_METHODS)}" + ) + if ( + thresholds_smoothing == QuickAdapterRegressorV3._CUSTOM_THRESHOLD_METHODS[0] + ): # "median" + return QuickAdapterRegressorV3.median_min_max( + pred_extrema, extrema_selection + ) + elif ( + thresholds_smoothing == QuickAdapterRegressorV3._CUSTOM_THRESHOLD_METHODS[1] + ): # "soft_extremum" thresholds_alpha = float(predictions_extrema.get("thresholds_alpha", 12.0)) return QuickAdapterRegressorV3.soft_extremum_min_max( pred_extrema, thresholds_alpha, extrema_selection ) - elif thresholds_smoothing in skimage_thresholds_smoothing_methods: + else: return QuickAdapterRegressorV3.skimage_min_max( pred_extrema, thresholds_smoothing, extrema_selection ) - else: - raise ValueError( - f"Unsupported thresholds smoothing method: {thresholds_smoothing}. Supported methods are {', '.join(thresholds_smoothing_methods)}" - ) @staticmethod def get_pred_min_max( @@ -866,36 +903,48 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return soft_minimum, soft_maximum @staticmethod - def skimage_min_max( + def median_min_max( pred_extrema: pd.Series, - method: str, extrema_selection: ExtremaSelectionMethod, ) -> tuple[float, float]: pred_minima, pred_maxima = QuickAdapterRegressorV3.get_pred_min_max( pred_extrema, extrema_selection ) - method_functions = { - "isodata": QuickAdapterRegressorV3.apply_skimage_threshold, - "li": QuickAdapterRegressorV3.apply_skimage_threshold, - "mean": QuickAdapterRegressorV3.apply_skimage_threshold, - "minimum": QuickAdapterRegressorV3.apply_skimage_threshold, - "otsu": QuickAdapterRegressorV3.apply_skimage_threshold, - "triangle": QuickAdapterRegressorV3.apply_skimage_threshold, - "yen": QuickAdapterRegressorV3.apply_skimage_threshold, - } + if pred_minima.empty: + min_val = np.nan + else: + min_val = np.median(pred_minima.to_numpy()) + if not np.isfinite(min_val): + min_val = QuickAdapterRegressorV3.safe_min_pred(pred_extrema) - if method not in method_functions: - raise ValueError(f"Unsupported method: {method}") + if pred_maxima.empty: + max_val = np.nan + else: + max_val = np.median(pred_maxima.to_numpy()) + if not np.isfinite(max_val): + max_val = QuickAdapterRegressorV3.safe_max_pred(pred_extrema) - min_func = method_functions[method] - max_func = method_functions[method] + return min_val, max_val + + @staticmethod + def skimage_min_max( + pred_extrema: pd.Series, + method: str, + extrema_selection: ExtremaSelectionMethod, + ) -> tuple[float, float]: + pred_minima, pred_maxima = QuickAdapterRegressorV3.get_pred_min_max( + pred_extrema, extrema_selection + ) try: threshold_func = getattr(skimage.filters, f"threshold_{method}") except AttributeError: raise ValueError(f"Unknown skimage threshold function: threshold_{method}") + min_func = QuickAdapterRegressorV3.apply_skimage_threshold + max_func = QuickAdapterRegressorV3.apply_skimage_threshold + min_val = min_func(pred_minima, threshold_func) if not np.isfinite(min_val): min_val = QuickAdapterRegressorV3.safe_min_pred(pred_extrema) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 4acfe56..50ba6d0 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -1066,8 +1066,8 @@ class QuickAdapterV3(IStrategy): self, df: DataFrame, trade: Trade, exit_stage: int ) -> Optional[float]: natr_ratio_percent = ( - self.partial_exit_stages[exit_stage][0] - if exit_stage in self.partial_exit_stages + QuickAdapterV3.partial_exit_stages[exit_stage][0] + if exit_stage in QuickAdapterV3.partial_exit_stages else 1.0 ) take_profit_distance = self.get_take_profit_distance( @@ -1187,7 +1187,7 @@ class QuickAdapterV3(IStrategy): return None trade_exit_stage = QuickAdapterV3.get_trade_exit_stage(trade) - if trade_exit_stage not in self.partial_exit_stages: + if trade_exit_stage not in QuickAdapterV3.partial_exit_stages: return None df, _ = self.dp.get_analyzed_dataframe( @@ -1219,7 +1219,9 @@ class QuickAdapterV3(IStrategy): min_stake = 0.0 if min_stake > trade.stake_amount: return None - trade_stake_percent = self.partial_exit_stages[trade_exit_stage][1] + trade_stake_percent = QuickAdapterV3.partial_exit_stages[trade_exit_stage][ + 1 + ] trade_partial_stake_amount = trade_stake_percent * trade.stake_amount remaining_stake_amount = trade.stake_amount - trade_partial_stake_amount if remaining_stake_amount < min_stake: @@ -1481,9 +1483,9 @@ class QuickAdapterV3(IStrategy): """ if df.empty: return False - if side not in self._trade_directions_set(): + if side not in QuickAdapterV3._trade_directions_set(): return False - if order not in self._order_types_set(): + if order not in QuickAdapterV3._order_types_set(): return False trade_direction = side @@ -1829,7 +1831,7 @@ class QuickAdapterV3(IStrategy): return "maxima_detected_long" trade_exit_stage = QuickAdapterV3.get_trade_exit_stage(trade) - if trade_exit_stage in self.partial_exit_stages: + if trade_exit_stage in QuickAdapterV3.partial_exit_stages: return None trade_take_profit_price = self.get_take_profit_price( @@ -1927,7 +1929,7 @@ class QuickAdapterV3(IStrategy): side: str, **kwargs, ) -> bool: - if side not in self._trade_directions_set(): + if side not in QuickAdapterV3._trade_directions_set(): return False if ( side == QuickAdapterV3._TRADE_DIRECTIONS[1] and not self.can_short