]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
feat(qav3): add generic direction support for MO quantile trials
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 25 May 2025 17:22:39 +0000 (19:22 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 25 May 2025 17:22:39 +0000 (19:22 +0200)
selection  method

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py

index 19fbb756451d0121b05dc209bfed0e3ed2eda2b3..689bc4059ae4babbd6ade19477d4dd7292a6c458 100644 (file)
@@ -45,7 +45,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     https://github.com/sponsors/robcaulk
     """
 
-    version = "3.7.68"
+    version = "3.7.69"
 
     @cached_property
     def _optuna_config(self) -> dict:
@@ -429,52 +429,169 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             if len(best_trials) == 1:
                 return best_trials[0]
 
+            def compare_primary_objective(
+                trial_a: optuna.trial.FrozenTrial,
+                trial_b: optuna.trial.FrozenTrial,
+                direction: optuna.study.StudyDirection,
+            ) -> bool:
+                if direction == optuna.study.StudyDirection.MAXIMIZE:
+                    return trial_a.values[0] > trial_b.values[0]
+                return trial_a.values[0] < trial_b.values[0]
+
+            def is_better_above_candidate(
+                candidate_trial: optuna.trial.FrozenTrial,
+                previous_trial: Optional[optuna.trial.FrozenTrial],
+                direction: optuna.study.StudyDirection,
+                target_pivot_size: float,
+            ) -> bool:
+                if not previous_trial:
+                    return True
+
+                candidate_distance = candidate_trial.values[1] - target_pivot_size
+                previous_distance = previous_trial.values[1] - target_pivot_size
+
+                if not np.isclose(candidate_distance, previous_distance):
+                    return candidate_distance < previous_distance
+
+                return compare_primary_objective(
+                    candidate_trial, previous_trial, direction
+                )
+
+            def is_better_below_candidate(
+                candidate_trial: optuna.trial.FrozenTrial,
+                previous_trial: Optional[optuna.trial.FrozenTrial],
+                direction: optuna.study.StudyDirection,
+                target_pivot_size: float,
+            ) -> bool:
+                if not previous_trial:
+                    return True
+
+                candidate_distance = target_pivot_size - candidate_trial.values[1]
+                previous_distance = target_pivot_size - previous_trial.values[1]
+
+                if not np.isclose(candidate_distance, previous_distance):
+                    return candidate_distance < previous_distance
+
+                return compare_primary_objective(
+                    candidate_trial, previous_trial, direction
+                )
+
             pivots_sizes = [trial.values[1] for trial in best_trials]
             quantile_pivots_size = np.quantile(
                 pivots_sizes, self.ft_params.get("label_quantile", 0.5)
             )
 
+            direction0 = study.directions[0]
+
             equal_quantile_pivots_size_trials = [
                 trial
                 for trial in best_trials
                 if np.isclose(trial.values[1], quantile_pivots_size)
             ]
             if equal_quantile_pivots_size_trials:
-                return max(
-                    equal_quantile_pivots_size_trials, key=lambda trial: trial.values[0]
-                )
-            nearest_above_quantile = (
-                np.inf,
-                -np.inf,
-                None,
-            )  # (trial_pivots_size, trial_scaled_natr, trial_index)
-            nearest_below_quantile = (
-                -np.inf,
-                -np.inf,
-                None,
-            )  # (trial_pivots_size, trial_scaled_natr, trial_index)
-            for idx, trial in enumerate(best_trials):
+                if direction0 == optuna.study.StudyDirection.MAXIMIZE:
+                    return max(
+                        equal_quantile_pivots_size_trials,
+                        key=lambda trial: trial.values[0],
+                    )
+                else:
+                    return min(
+                        equal_quantile_pivots_size_trials,
+                        key=lambda trial: trial.values[0],
+                    )
+
+            nearest_above_quantile = None
+            nearest_below_quantile = None
+            for trial in best_trials:
                 pivots_size = trial.values[1]
+
                 if pivots_size >= quantile_pivots_size:
-                    if pivots_size < nearest_above_quantile[0] or (
-                        pivots_size == nearest_above_quantile[0]
-                        and trial.values[0] > nearest_above_quantile[1]
+                    if is_better_above_candidate(
+                        trial, nearest_above_quantile, direction0, quantile_pivots_size
                     ):
-                        nearest_above_quantile = (pivots_size, trial.values[0], idx)
+                        nearest_above_quantile = trial
+
                 if pivots_size <= quantile_pivots_size:
-                    if pivots_size > nearest_below_quantile[0] or (
-                        pivots_size == nearest_below_quantile[0]
-                        and trial.values[0] > nearest_below_quantile[1]
+                    if is_better_below_candidate(
+                        trial, nearest_below_quantile, direction0, quantile_pivots_size
                     ):
-                        nearest_below_quantile = (pivots_size, trial.values[0], idx)
-            if nearest_above_quantile[2] is None or nearest_below_quantile[2] is None:
+                        nearest_below_quantile = trial
+
+            if not nearest_above_quantile and not nearest_below_quantile:
                 return None
-            above_quantile_trial = best_trials[nearest_above_quantile[2]]
-            below_quantile_trial = best_trials[nearest_below_quantile[2]]
-            if above_quantile_trial.values[0] >= below_quantile_trial.values[0]:
-                return above_quantile_trial
+            if not nearest_above_quantile:
+                return nearest_below_quantile
+            if not nearest_below_quantile:
+                return nearest_above_quantile
+
+            if direction0 == optuna.study.StudyDirection.MAXIMIZE:
+                if np.isclose(
+                    nearest_above_quantile.values[0], nearest_below_quantile.values[0]
+                ):
+                    above_quantile_distance = (
+                        nearest_above_quantile.values[1] - quantile_pivots_size
+                    )
+                    below_quantile_distance = (
+                        quantile_pivots_size - nearest_below_quantile.values[1]
+                    )
+
+                    if abs(above_quantile_distance) < abs(below_quantile_distance):
+                        return nearest_above_quantile
+                    elif abs(above_quantile_distance) > abs(below_quantile_distance):
+                        return nearest_below_quantile
+                    else:
+                        direction1 = study.directions[1]
+                        if direction1 == optuna.study.StudyDirection.MAXIMIZE:
+                            return max(
+                                [nearest_above_quantile, nearest_below_quantile],
+                                key=lambda trial: trial.values[1],
+                            )
+                        else:
+                            return min(
+                                [nearest_above_quantile, nearest_below_quantile],
+                                key=lambda trial: trial.values[1],
+                            )
+                else:
+                    return (
+                        nearest_above_quantile
+                        if nearest_above_quantile.values[0]
+                        > nearest_below_quantile.values[0]
+                        else nearest_below_quantile
+                    )
             else:
-                return below_quantile_trial
+                if np.isclose(
+                    nearest_above_quantile.values[0], nearest_below_quantile.values[0]
+                ):
+                    above_quantile_distance = (
+                        nearest_above_quantile.values[1] - quantile_pivots_size
+                    )
+                    below_quantile_distance = (
+                        quantile_pivots_size - nearest_below_quantile.values[1]
+                    )
+
+                    if abs(above_quantile_distance) < abs(below_quantile_distance):
+                        return nearest_above_quantile
+                    elif abs(above_quantile_distance) > abs(below_quantile_distance):
+                        return nearest_below_quantile
+                    else:
+                        direction1 = study.directions[1]
+                        if direction1 == optuna.study.StudyDirection.MAXIMIZE:
+                            return max(
+                                [nearest_above_quantile, nearest_below_quantile],
+                                key=lambda trial: trial.values[1],
+                            )
+                        else:
+                            return min(
+                                [nearest_above_quantile, nearest_below_quantile],
+                                key=lambda trial: trial.values[1],
+                            )
+                else:
+                    return (
+                        nearest_above_quantile
+                        if nearest_above_quantile.values[0]
+                        < nearest_below_quantile.values[0]
+                        else nearest_below_quantile
+                    )
         elif label_trials_selection == "chebyshev":
             objective_values = np.array([trial.values for trial in best_trials]).T
             normalized_values_list = []
index 24d17d63fedb26fa3dbdab3cd2a371ef33b39e44..5ace30f8164a4b87be608d090a5e3962c0f9340a 100644 (file)
@@ -60,7 +60,7 @@ class QuickAdapterV3(IStrategy):
     INTERFACE_VERSION = 3
 
     def version(self) -> str:
-        return "3.3.70"
+        return "3.3.71"
 
     timeframe = "5m"