From: Jérôme Benoit Date: Thu, 30 Jul 2026 17:22:55 +0000 (+0200) Subject: refactor(quickadapter): deduplicate regressor (min,max) finite fallback (#176) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=6f28a959738289480babf457f7779c59f7859d57;p=freqai-strategies.git refactor(quickadapter): deduplicate regressor (min,max) finite fallback (#176) * 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. --- diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 513e8b7..aa96e82 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -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(