]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
refactor(quickadapter): deduplicate regressor (min,max) finite fallback (#176)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 30 Jul 2026 17:22:55 +0000 (19:22 +0200)
committerGitHub <noreply@github.com>
Thu, 30 Jul 2026 17:22:55 +0000 (19:22 +0200)
* refactor(quickadapter): deduplicate regressor (min,max) finite fallback

Extract `_resolve_min_max` applying the finite fallback once and reuse it
from `soft_extremum_min_max`, `median_min_max`, and `skimage_min_max`.
Fold the `safe_min_pred`/`safe_max_pred` twins into a `_safe_pred` core
with two thin wrappers passing the load-bearing +/-2.0 sentinels explicitly.

The finite branch returns the candidate unchanged (no float() coercion),
preserving dtype; only the non-finite branch routes through the unchanged
safe_*_pred fallback. Outputs are bit-for-bit unchanged (verified in
quickadapter-freqtrade:latest across 38 finite/non-finite edge cases).

* docs(quickadapter): document dtype-preserving fallback invariant

Add a comment on `_resolve_min_max` capturing the load-bearing invariant
surfaced during review: finite candidates are returned without float()
coercion to preserve their dtype, and the ±2.0 sentinels are the
out-of-domain bounds of normalized labels. Behavior unchanged; bit-for-bit
equivalence re-verified in quickadapter-freqtrade:latest (38 edge cases,
0 divergence).

* docs(quickadapter): scope fallback sentinel comment to default label range

Refine the `_resolve_min_max` comment surfaced in re-review: the ±2.0
sentinels sit outside the default [-1, 1] normalized label range (not
universally, since normalization="none" and custom minmax_range are
unbounded), and the no-float()-coercion note is tied to preserving the
pre-refactor bit-for-bit behavior rather than an incidental dtype.
Comment-only; behavior re-verified bit-for-bit in the container.

* docs(quickadapter): harmonize fallback comment code formatting

Wrap `float()` in RST double backticks in the `_resolve_min_max` comment
to match the class's explanatory-comment convention for code identifiers.
Comment-only; behavior re-verified bit-for-bit in the container.

* docs(quickadapter): relocate regressor fallback sentinel comment

Drop the historical (pre-refactor) finite-passthrough comment in
_resolve_min_max, whose behavior is self-evident from the code, and
document the surviving non-evident invariant (±2.0 fallbacks are
out-of-[-1, 1] normalized-range sentinels) at the safe_min_pred/
safe_max_pred definition site.

quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py

index 513e8b74619c1ad5e0067716c7a6f452c6f09fe2..aa96e82c5f24b6f79585979be873aca58d9e27e4 100644 (file)
@@ -3544,32 +3544,47 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         return pred_label_minima, pred_label_maxima
 
     @staticmethod
-    def safe_min_pred(pred_label: pd.Series) -> float:
+    def _safe_pred(
+        pred_label: pd.Series,
+        reducer: Callable[[pd.Series], float],
+        fallback: float,
+    ) -> float:
         try:
-            pred_label_minimum = pred_label.min()
+            reduced = reducer(pred_label)
         except Exception:
-            pred_label_minimum = None
+            reduced = None
         if (
-            pred_label_minimum is not None
-            and isinstance(pred_label_minimum, (int, float, np.number))
-            and np.isfinite(pred_label_minimum)
+            reduced is not None
+            and isinstance(reduced, (int, float, np.number))
+            and np.isfinite(reduced)
         ):
-            return float(pred_label_minimum)
-        return -2.0
+            return float(reduced)
+        return fallback
+
+    # ±2.0 fallbacks are out-of-[-1, 1] normalized-range sentinels.
+    @staticmethod
+    def safe_min_pred(pred_label: pd.Series) -> float:
+        return QuickAdapterRegressorV3._safe_pred(
+            pred_label, lambda series: series.min(), -2.0
+        )
 
     @staticmethod
     def safe_max_pred(pred_label: pd.Series) -> float:
-        try:
-            pred_label_maximum = pred_label.max()
-        except Exception:
-            pred_label_maximum = None
-        if (
-            pred_label_maximum is not None
-            and isinstance(pred_label_maximum, (int, float, np.number))
-            and np.isfinite(pred_label_maximum)
-        ):
-            return float(pred_label_maximum)
-        return 2.0
+        return QuickAdapterRegressorV3._safe_pred(
+            pred_label, lambda series: series.max(), 2.0
+        )
+
+    @staticmethod
+    def _resolve_min_max(
+        min_candidate: float,
+        max_candidate: float,
+        pred_label: pd.Series,
+    ) -> tuple[float, float]:
+        if not np.isfinite(min_candidate):
+            min_candidate = QuickAdapterRegressorV3.safe_min_pred(pred_label)
+        if not np.isfinite(max_candidate):
+            max_candidate = QuickAdapterRegressorV3.safe_max_pred(pred_label)
+        return min_candidate, max_candidate
 
     @staticmethod
     def soft_extremum_min_max(
@@ -3584,12 +3599,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             pred_label, selection_method, keep_fraction
         )
         soft_minimum = soft_extremum(pred_label_minima, alpha=-alpha)
-        if not np.isfinite(soft_minimum):
-            soft_minimum = QuickAdapterRegressorV3.safe_min_pred(pred_label)
         soft_maximum = soft_extremum(pred_label_maxima, alpha=alpha)
-        if not np.isfinite(soft_maximum):
-            soft_maximum = QuickAdapterRegressorV3.safe_max_pred(pred_label)
-        return soft_minimum, soft_maximum
+        return QuickAdapterRegressorV3._resolve_min_max(
+            soft_minimum, soft_maximum, pred_label
+        )
 
     @staticmethod
     def median_min_max(
@@ -3605,17 +3618,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             min_val = np.nan
         else:
             min_val = np.nanmedian(pred_label_minima.to_numpy())
-        if not np.isfinite(min_val):
-            min_val = QuickAdapterRegressorV3.safe_min_pred(pred_label)
 
         if pred_label_maxima.empty:
             max_val = np.nan
         else:
             max_val = np.nanmedian(pred_label_maxima.to_numpy())
-        if not np.isfinite(max_val):
-            max_val = QuickAdapterRegressorV3.safe_max_pred(pred_label)
 
-        return min_val, max_val
+        return QuickAdapterRegressorV3._resolve_min_max(min_val, max_val, pred_label)
 
     @staticmethod
     def skimage_min_max(
@@ -3640,14 +3649,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         max_func = QuickAdapterRegressorV3.apply_skimage_threshold
 
         min_val = min_func(pred_label_minima, threshold_func)
-        if not np.isfinite(min_val):
-            min_val = QuickAdapterRegressorV3.safe_min_pred(pred_label)
-
         max_val = max_func(pred_label_maxima, threshold_func)
-        if not np.isfinite(max_val):
-            max_val = QuickAdapterRegressorV3.safe_max_pred(pred_label)
 
-        return min_val, max_val
+        return QuickAdapterRegressorV3._resolve_min_max(min_val, max_val, pred_label)
 
     @staticmethod
     def apply_skimage_threshold(