]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
refactor: cleanup tunables handling
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 20 Nov 2025 21:41:32 +0000 (22:41 +0100)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 20 Nov 2025 21:41:32 +0000 (22:41 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
README.md
ReforceXY/user_data/freqaimodels/ReforceXY.py
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py

index 07b0f2abd1fb1784d68c382e954f1d4234503eb4..2604725a4cef8c33b8702d4965301620958ec568 100644 (file)
--- 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_                                       |                   |                                                                                                                                  |                                                                                                                                                                                                            |
index 8151564f1a751888dbdd34e4402a0eba1c6a14a7..3058e2150f233c1ff791b9d71b49e6f633f1b69c 100644 (file)
@@ -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)
 
 
index 2f45f92124b16080a20c9e54cc1847db7dcc7ef7..007d580376e07f5e4922359fcc4d98956d88ec43 100644 (file)
@@ -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)
index 4acfe56d0f0398f8a2711ce7e86930065c5cb7e8..50ba6d0cb3a520004067fb8a79231ba9df249629 100644 (file)
@@ -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