]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(quickadapter): preserve label params per candle (#112)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 23 Jul 2026 18:31:00 +0000 (20:31 +0200)
committerGitHub <noreply@github.com>
Thu, 23 Jul 2026 18:31:00 +0000 (20:31 +0200)
* fix(quickadapter): preserve label params per candle

* fix(quickadapter): reject invalid dynamic label params

* refactor(quickadapter): harden label-param validation and dedupe runmode gate

- add _is_finite_number guard rejecting bool and non-numeric before
  np.isfinite (which raised on str/object) across the label getters/setters
- factor the duplicated runmode-in-TRADE_MODES predicate into the
  is_trade_runmode cached_property
- document the per-candle NATR construction and drop the redundant fillna
  after where() in the per-row NATR path
- note that backtest and hyperopt (not only backtest) retain the per-candle
  label period in the README

README.md
quickadapter/user_data/strategies/QuickAdapterV3.py

index e8c83c43d5b354c639e409ecac3c408643dfdd03..b2bcc1de84cd3b11103b39d339f0f198f3c605b2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -100,12 +100,12 @@ docker compose up -d --build
 | freqai.label_pipeline.sigmoid_scale                            | 1.0                           | float > 0                                                                                                                                                                                                    | Scale parameter for `sigmoid` normalization, controls steepness.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
 | freqai.label_pipeline.gamma                                    | 1.0                           | float (0,10]                                                                                                                                                                                                 | Contrast exponent applied to labels after normalization: >1 emphasizes extrema, values between 0 and 1 soften.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
 | _Feature parameters_                                           |                               |                                                                                                                                                                                                              |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
-| freqai.feature_parameters.label_period_candles                 | min/max midpoint              | int >= 1                                                                                                                                                                                                     | Zigzag labeling NATR period.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+| freqai.feature_parameters.label_period_candles                 | min/max midpoint              | int >= 1                                                                                                                                                                                                     | Zigzag labeling NATR period. When label HPO is enabled, backtest and hyperopt retain the period produced for each simulated candle when calculating strategy NATR.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
 | freqai.feature_parameters.label_horizon_candles                | `label_period_candles`        | int >= 1                                                                                                                                                                                                     | Number of candles after a label row before the label is considered known by causal split guards. Recommended: cover the zigzag pivot confirmation lag (the smoothing kernel half-width is added automatically by `set_freqai_targets`). Used by causal split guards and `<label>_known_at_lookahead` metadata. When unset, falls back to `label_period_candles`.                                                                                                                                                                                                                                           |
 | freqai.feature_parameters.causal_mode                          | true                          | bool                                                                                                                                                                                                         | Causal split guard toggle. When `true` (default): rejects `data_split_parameters.shuffle=true`, `shuffle_after_split=true`, `reverse_train_test_order=true`; for `timeseries_split` auto-sets `gap=label_horizon_candles` when unset/`0` (rejects explicit `gap<label_horizon_candles`); for `train_test_split` drops train rows where position `>=first_test_position-label_horizon_candles`; with `<label>_known_at_lookahead` columns, additionally drops rows where `local_position + row-wise max(<label>_known_at_lookahead) >= first_test_position`. `false` is deprecated; acausal baselines only. |
 | freqai.feature_parameters.min_label_period_candles             | 12                            | int >= 1                                                                                                                                                                                                     | Minimum labeling NATR period used for reversals labeling HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
 | freqai.feature_parameters.max_label_period_candles             | 24                            | int >= 1                                                                                                                                                                                                     | Maximum labeling NATR period used for reversals labeling HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
-| freqai.feature_parameters.label_natr_multiplier                | min/max midpoint              | float > 0                                                                                                                                                                                                    | Zigzag labeling NATR multiplier.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
+| freqai.feature_parameters.label_natr_multiplier                | min/max midpoint              | float > 0                                                                                                                                                                                                    | Zigzag labeling NATR multiplier. When label HPO is enabled, stop-loss, take-profit, and candle-deviation calculations consume the value produced for the current simulated candle.                                                                                                                                                                                                                                                                                                                                                                                                                             |
 | freqai.feature_parameters.min_label_natr_multiplier            | 9.0                           | float > 0                                                                                                                                                                                                    | Minimum labeling NATR multiplier used for reversals labeling HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
 | freqai.feature_parameters.max_label_natr_multiplier            | 12.0                          | float > 0                                                                                                                                                                                                    | Maximum labeling NATR multiplier used for reversals labeling HPO.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
 | freqai.feature_parameters.label_frequency_candles              | `auto`                        | int >= 2 \| `auto`                                                                                                                                                                                           | Reversals labeling frequency. `auto` = max(2, 2 \* number of whitelisted pairs).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
index 967d53fddef68ab68fe799d5d2a3b06c02d6c06a..88c09e74161eb59cb495568e911403288cc218af 100644 (file)
@@ -200,6 +200,12 @@ class QuickAdapterV3(IStrategy):
     def timeframe_minutes(self) -> int:
         return timeframe_to_minutes(self.config.get("timeframe"))
 
+    @cached_property
+    def is_trade_runmode(self) -> bool:
+        # Mirror the regressor's ``self.live`` gate (runmode in TRADE_MODES):
+        # persisted label params are reused only in live and dry-run.
+        return self.config.get("runmode") in TRADE_MODES
+
     @property
     def can_short(self) -> bool:
         return self.is_short_allowed()
@@ -455,9 +461,7 @@ class QuickAdapterV3(IStrategy):
             self._label_defaults
         )
         self._label_params: dict[str, dict[str, Any]] = {}
-        # Mirror the regressor's ``self.live`` gate (runmode in TRADE_MODES):
-        # persisted label params are reused only in live and dry-run.
-        load_persisted_label_params = self.config.get("runmode") in TRADE_MODES
+        load_persisted_label_params = self.is_trade_runmode
         for pair in self.pairs:
             label_best_params = (
                 self.optuna_load_best_params(pair, _OPTUNA_NAMESPACES.label)
@@ -857,20 +861,41 @@ class QuickAdapterV3(IStrategy):
         dataframe["%-hour_of_day"] = (dates.dt.hour + 1) / 25
         return dataframe
 
-    def get_label_period_candles(self, pair: str) -> int:
-        label_period_candles = self._label_params.get(pair, {}).get(
-            "label_period_candles"
+    @staticmethod
+    def _is_finite_number(value: Any) -> bool:
+        # Reject bool (int(True) == 1) and non-numeric (str/object) before
+        # np.isfinite, which raises on non-numeric input.
+        return (
+            not isinstance(value, bool)
+            and isinstance(value, (int, float, np.integer, np.floating))
+            and bool(np.isfinite(value))
         )
-        if label_period_candles and isinstance(label_period_candles, int):
-            return label_period_candles
-        return self.freqai_info.get("feature_parameters", {}).get(
-            "label_period_candles",
-            self._label_defaults[0],
+
+    def get_label_period_candles(
+        self,
+        pair: str,
+        dataframe: Optional[DataFrame] = None,
+        candle_idx: int = -1,
+    ) -> int:
+        if dataframe is not None:
+            period_series = dataframe.get("label_period_candles")
+            if period_series is not None and not period_series.empty:
+                period = period_series.iloc[candle_idx]
+                if self._is_finite_number(period) and int(period) > 0:
+                    return int(period)
+        period = self._label_params.get(pair, {}).get("label_period_candles")
+        return int(
+            period
+            if period is not None
+            else self.freqai_info.get("feature_parameters", {}).get(
+                "label_period_candles",
+                self._label_defaults[0],
+            )
         )
 
-    def set_label_period_candles(self, pair: str, label_period_candles: int) -> None:
-        if isinstance(label_period_candles, int):
-            self._label_params[pair]["label_period_candles"] = label_period_candles
+    def set_label_period_candles(self, pair: str, label_period_candles: Any) -> None:
+        if self._is_finite_number(label_period_candles) and int(label_period_candles) > 0:
+            self._label_params[pair]["label_period_candles"] = int(label_period_candles)
 
     def get_label_horizon_candles(self, pair: str) -> int:
         period = self.get_label_period_candles(pair)
@@ -881,31 +906,45 @@ class QuickAdapterV3(IStrategy):
             logger,
         )
 
-    def get_label_natr_multiplier(self, pair: str) -> float:
-        label_natr_multiplier = self._label_params.get(pair, {}).get(
-            "label_natr_multiplier"
-        )
-        if label_natr_multiplier and isinstance(label_natr_multiplier, float):
-            return label_natr_multiplier
-        feature_parameters = self.freqai_info.get("feature_parameters", {})
+    def get_label_natr_multiplier(
+        self,
+        pair: str,
+        dataframe: Optional[DataFrame] = None,
+        candle_idx: int = -1,
+    ) -> float:
+        if dataframe is not None:
+            multiplier_series = dataframe.get("label_natr_multiplier")
+            if multiplier_series is not None and not multiplier_series.empty:
+                multiplier = multiplier_series.iloc[candle_idx]
+                if self._is_finite_number(multiplier) and float(multiplier) > 0.0:
+                    return float(multiplier)
+        multiplier = self._label_params.get(pair, {}).get("label_natr_multiplier")
         return float(
-            feature_parameters.get("label_natr_multiplier", self._label_defaults[1])
+            multiplier
+            if multiplier is not None
+            else self.freqai_info.get("feature_parameters", {}).get(
+                "label_natr_multiplier", self._label_defaults[1]
+            )
         )
 
-    def set_label_natr_multiplier(
-        self, pair: str, label_natr_multiplier: float
-    ) -> None:
-        if isinstance(label_natr_multiplier, float) and np.isfinite(
-            label_natr_multiplier
-        ):
-            self._label_params[pair]["label_natr_multiplier"] = label_natr_multiplier
+    def set_label_natr_multiplier(self, pair: str, label_natr_multiplier: Any) -> None:
+        if self._is_finite_number(label_natr_multiplier) and float(label_natr_multiplier) > 0.0:
+            self._label_params[pair]["label_natr_multiplier"] = float(
+                label_natr_multiplier
+            )
 
-    def get_label_natr_multiplier_fraction(self, pair: str, fraction: float) -> float:
+    def get_label_natr_multiplier_fraction(
+        self,
+        pair: str,
+        fraction: float,
+        dataframe: Optional[DataFrame] = None,
+        candle_idx: int = -1,
+    ) -> float:
         if not isinstance(fraction, float) or not (0.0 <= fraction <= 1.0):
             raise ValueError(
                 f"Invalid fraction value {fraction!r}: must be a float in range [0, 1]"
             )
-        return self.get_label_natr_multiplier(pair) * fraction
+        return self.get_label_natr_multiplier(pair, dataframe, candle_idx) * fraction
 
     def get_label_params(self, pair: str, label_col: str) -> dict[str, Any]:
         if label_col == EXTREMA_COLUMN:
@@ -1043,15 +1082,39 @@ class QuickAdapterV3(IStrategy):
         pair = str(metadata.get("pair"))
 
         label_period_candles_series = dataframe.get("label_period_candles")
-        if label_period_candles_series is not None:
-            self.set_label_period_candles(pair, label_period_candles_series.iloc[-1])
         label_natr_multiplier_series = dataframe.get("label_natr_multiplier")
-        if label_natr_multiplier_series is not None:
-            self.set_label_natr_multiplier(pair, label_natr_multiplier_series.iloc[-1])
+        if self.is_trade_runmode:
+            if label_period_candles_series is not None:
+                self.set_label_period_candles(
+                    pair, label_period_candles_series.iloc[-1]
+                )
+            if label_natr_multiplier_series is not None:
+                self.set_label_natr_multiplier(
+                    pair, label_natr_multiplier_series.iloc[-1]
+                )
 
-        dataframe["natr_label_period_candles"] = ta.NATR(
-            dataframe, timeperiod=self.get_label_period_candles(pair)
-        )
+        if label_period_candles_series is None:
+            dataframe["natr_label_period_candles"] = ta.NATR(
+                dataframe, timeperiod=self.get_label_period_candles(pair)
+            )
+        else:
+            # Per-candle HPO label_period_candles: NATR is computed once per
+            # distinct period, then scattered back to its matching rows (mixing
+            # per-row periods within one column is intentional).
+            dataframe["natr_label_period_candles"] = np.nan
+            fallback_period = self.get_label_period_candles(pair)
+            valid_periods = np.isfinite(label_period_candles_series) & (
+                label_period_candles_series >= 1
+            )
+            periods = label_period_candles_series.where(
+                valid_periods, fallback_period
+            ).astype(int)
+            for period in periods.unique():
+                period_rows = periods == period
+                period_natr = ta.NATR(dataframe, timeperiod=int(period))
+                dataframe.loc[period_rows, "natr_label_period_candles"] = (
+                    period_natr.loc[period_rows]
+                )
 
         dataframe["minima_threshold"] = dataframe.get(
             f"{EXTREMA_COLUMN}_minima_threshold", np.nan
@@ -1313,7 +1376,7 @@ class QuickAdapterV3(IStrategy):
             current_rate
             * (trade_natr / 100.0)
             * self.get_label_natr_multiplier_fraction(
-                trade.pair, natr_multiplier_fraction
+                trade.pair, natr_multiplier_fraction, df
             )
             * QuickAdapterV3.get_stoploss_factor(
                 trade_duration_candles + int(round(trade.nr_of_successful_exits**1.5))
@@ -1342,7 +1405,7 @@ class QuickAdapterV3(IStrategy):
             trade.open_rate
             * (trade_natr / 100.0)
             * self.get_label_natr_multiplier_fraction(
-                trade.pair, natr_multiplier_fraction
+                trade.pair, natr_multiplier_fraction, df
             )
             * QuickAdapterV3.get_take_profit_factor(trade_duration_candles)
         )
@@ -1659,7 +1722,7 @@ class QuickAdapterV3(IStrategy):
         candle_label_natr_value = label_natr_values[-1]
         if isna(candle_label_natr_value) or candle_label_natr_value < 0:
             return np.nan
-        label_period_candles = self.get_label_period_candles(pair)
+        label_period_candles = self.get_label_period_candles(pair, df, candle_idx)
         candle_label_natr_value_quantile = calculate_quantile(
             label_natr_values[-label_period_candles:], candle_label_natr_value
         )
@@ -1689,7 +1752,9 @@ class QuickAdapterV3(IStrategy):
             )
         candle_deviation = (
             candle_label_natr_value / 100.0
-        ) * self.get_label_natr_multiplier_fraction(pair, natr_multiplier_fraction)
+        ) * self.get_label_natr_multiplier_fraction(
+            pair, natr_multiplier_fraction, df, candle_idx
+        )
         self._candle_deviation_cache[cache_key] = candle_deviation
         return self._candle_deviation_cache[cache_key]