]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
refactor(quickadapter): route inline enum error messages through enum_error_message...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Fri, 31 Jul 2026 13:07:07 +0000 (15:07 +0200)
committerGitHub <noreply@github.com>
Fri, 31 Jul 2026 13:07:07 +0000 (15:07 +0200)
* refactor(quickadapter): relocate enum_error_message to EnumErrors module

Extract the canonical enum validation error formatter into a new
dependency-free module (strategies/EnumErrors.py) and re-export it from
Utils. This lets LabelTransformer route through the same helper without
importing Utils (which would create an import cycle, since Utils imports
from LabelTransformer).

Behavior-preserving: the helper is moved verbatim; the public import path
'from Utils import enum_error_message' is preserved via re-export.

* refactor(quickadapter): route Utils enum errors through helper

Replace 5 inline 'Invalid X value ...: supported values are ...' error
constructions in Utils with enum_error_message calls (window type,
fill_bandwidth, fill_method, and two regressor sites). Byte-identical.

* refactor(quickadapter): route LabelTransformer enum errors through helper

Import enum_error_message from the dependency-free EnumErrors module
(LabelTransformer cannot import Utils without creating an import cycle)
and route the 3 inline enum error constructions (scaler family kind,
standardization, normalization). Byte-identical.

* refactor(quickadapter): route strategy enum errors through helper

Add enum_error_message to the existing 'from Utils import' block and
route the 4 inline enum error constructions in QuickAdapterV3
(trade_price_target_method, interpolation_direction, side, trading_mode).
Byte-identical.

* refactor(quickadapter): route regressor enum errors through helper

Route the 11 canonical inline enum error constructions plus 3 that keep
byte-identical output via explicit sequence wrapping:
- data_split_parameters.method, selection_method (x2), skimage threshold
  method, trial_selection_method, cluster_method, aggregation,
  label_method, optuna storage_backend, optuna sampler, namespace
- namespace single-value sites wrap (_OPTUNA_NAMESPACES.label,) so join
  yields the same string; optuna namespace-sampler wraps tuple(samplers)
  (frozenset -> Sequence[str]) preserving the existing join order.
Byte-identical.

* fix(quickadapter): unify divergent enum error wording via helper

Route the two enum error sites that did NOT include the word "value"
through enum_error_message, aligning them with the canonical format.

INTENTIONAL, observable message change (NOT byte-identical):
- QuickAdapterRegressorV3._validate_enum_value: 'Invalid {ctx} {value!r}:'
  -> 'Invalid {ctx} value {value!r}:'. The message is reused by the
  logger.warning at the same helper, so the change affects both the
  raised ValueError and the warning log across its 6 callers.
- Utils.get_ngboost_dist dist_name: 'Invalid dist_name {v!r}:' ->
  'Invalid dist_name value {v!r}:' (dict_keys wrapped in tuple() to
  satisfy Sequence[str]; join output unchanged).

No test asserts these strings (quickadapter has no test suite).

* style(quickadapter): harmonize EnumErrors import placement

Glue the first-party EnumErrors import directly to the preceding import
block in LabelTransformer, matching the convention already used in
Utils.py and QuickAdapterV3.py (no blank line separating first-party
from third-party imports). No behavior change.

* docs(quickadapter): scope EnumErrors docstring to the canonical form

The module owns the canonical 'Invalid <ctx> value <value>: supported
values are <options>' message; messages with custom prefix/infix/suffix
are built inline at their call sites. Avoids over-claiming a single
source of truth for every enum error string.

quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/EnumErrors.py [new file with mode: 0644]
quickadapter/user_data/strategies/LabelTransformer.py
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py

index 90f4d9efecf9667d83d721f9078441e48caeb46b..afe00bdde755f8e8d71777cb92d43130797e0d9e 100644 (file)
@@ -1079,9 +1079,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         if mode == "none":
             return default
 
-        msg = (
-            f"Invalid {ctx} {value!r}: supported values are {', '.join(valid_options)}"
-        )
+        msg = enum_error_message(ctx, value, valid_options)
         if mode == "raise":
             raise ValueError(msg)
         logger.warning(f"{msg}, using {default!r}")
@@ -1979,9 +1977,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 split_builder = self._make_timeseries_split_datasets
             case _:
                 raise ValueError(
-                    f"Invalid data_split_parameters.method value {method!r}: "
-                    f"supported values are "
-                    f"{', '.join(QuickAdapterRegressorV3._DATA_SPLIT_METHODS)}"
+                    enum_error_message(
+                        "data_split_parameters.method",
+                        method,
+                        QuickAdapterRegressorV3._DATA_SPLIT_METHODS,
+                    )
                 )
 
         def split_fn(
@@ -3098,8 +3098,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     ) -> None:
         if namespace not in {_OPTUNA_NAMESPACES.label}:
             raise ValueError(
-                f"Invalid namespace value {namespace!r}: "
-                f"supported values are {_OPTUNA_NAMESPACES.label}"
+                enum_error_message("namespace", namespace, (_OPTUNA_NAMESPACES.label,))
             )
         if not callable(callback):
             raise ValueError(
@@ -3564,8 +3563,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             pred_label_minima = pred_label[pred_label < -eps]
         else:
             raise ValueError(
-                f"Invalid selection_method value {selection_method!r}: "
-                f"supported values are {', '.join(EXTREMA_SELECTION_METHODS)}"
+                enum_error_message(
+                    "selection_method", selection_method, EXTREMA_SELECTION_METHODS
+                )
             )
 
         return pred_label_minima, pred_label_maxima
@@ -3668,8 +3668,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             threshold_func = getattr(skimage.filters, f"threshold_{method}")
         except AttributeError:
             raise ValueError(
-                f"Invalid skimage threshold method value {method!r}: "
-                f"supported values are {', '.join(SKIMAGE_THRESHOLD_METHODS)}"
+                enum_error_message(
+                    "skimage threshold method", method, SKIMAGE_THRESHOLD_METHODS
+                )
             )
 
         min_func = QuickAdapterRegressorV3.apply_skimage_threshold
@@ -4052,8 +4053,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
         else:
             raise ValueError(
-                f"Invalid trial_selection_method value {trial_selection_method!r}: "
-                f"supported values are {', '.join(QuickAdapterRegressorV3._DISTANCE_METHODS)}"
+                enum_error_message(
+                    "trial_selection_method",
+                    trial_selection_method,
+                    QuickAdapterRegressorV3._DISTANCE_METHODS,
+                )
             )
 
         min_score_position = np.nanargmin(scores)
@@ -4126,8 +4130,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 )
             else:
                 raise ValueError(
-                    f"Invalid selection_method value {selection_method!r}: "
-                    f"supported values are {', '.join(QuickAdapterRegressorV3._DISTANCE_METHODS)}"
+                    enum_error_message(
+                        "selection_method",
+                        selection_method,
+                        QuickAdapterRegressorV3._DISTANCE_METHODS,
+                    )
                 )
             ordered_cluster_indices = np.argsort(cluster_center_scores)
 
@@ -4164,8 +4171,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
 
         else:
             raise ValueError(
-                f"Invalid cluster_method value {cluster_method!r}: "
-                f"supported values are {', '.join(QuickAdapterRegressorV3._CLUSTER_METHODS)}"
+                enum_error_message(
+                    "cluster_method",
+                    cluster_method,
+                    QuickAdapterRegressorV3._CLUSTER_METHODS,
+                )
             )
 
     @staticmethod
@@ -4242,8 +4252,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             return np.nanmax(neighbor_distances, axis=1)
         else:
             raise ValueError(
-                f"Invalid aggregation value {aggregation!r}: "
-                f"supported values are {', '.join(QuickAdapterRegressorV3._DENSITY_AGGREGATIONS)}"
+                enum_error_message(
+                    "aggregation",
+                    aggregation,
+                    QuickAdapterRegressorV3._DENSITY_AGGREGATIONS,
+                )
             )
 
     @staticmethod
@@ -4580,8 +4593,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 )
 
         raise ValueError(
-            f"Invalid label_method value {selection_method!r}: "
-            f"supported values are {', '.join(QuickAdapterRegressorV3._SELECTION_METHODS)}"
+            enum_error_message(
+                "label_method",
+                selection_method,
+                QuickAdapterRegressorV3._SELECTION_METHODS,
+            )
         )
 
     def _get_multi_objective_study_best_trial(
@@ -4589,8 +4605,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     ) -> Optional[optuna.trial.FrozenTrial]:
         if namespace not in {_OPTUNA_NAMESPACES.label}:
             raise ValueError(
-                f"Invalid namespace value {namespace!r}: "
-                f"supported values are {_OPTUNA_NAMESPACES.label}"
+                enum_error_message("namespace", namespace, (_OPTUNA_NAMESPACES.label,))
             )
         n_objectives = len(study.directions)
         if n_objectives < 2:
@@ -4912,8 +4927,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
         else:
             raise ValueError(
-                f"Invalid optuna storage_backend value {storage_backend!r}: "
-                f"supported values are {', '.join(QuickAdapterRegressorV3._OPTUNA_STORAGE_BACKENDS)}"
+                enum_error_message(
+                    "optuna storage_backend",
+                    storage_backend,
+                    QuickAdapterRegressorV3._OPTUNA_STORAGE_BACKENDS,
+                )
             )
         return storage
 
@@ -4937,8 +4955,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         match sampler:
             case None:
                 raise ValueError(
-                    f"Invalid optuna sampler value {sampler!r}: "
-                    f"supported values are {', '.join(QuickAdapterRegressorV3._OPTUNA_SAMPLERS)}"
+                    enum_error_message(
+                        "optuna sampler",
+                        sampler,
+                        QuickAdapterRegressorV3._OPTUNA_SAMPLERS,
+                    )
                 )
             case QuickAdapterRegressorV3._OPTUNA_SAMPLERS.tpe:
                 return optuna.samplers.TPESampler(
@@ -4978,8 +4999,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             )
         else:
             raise ValueError(
-                f"Invalid namespace value {namespace!r}: "
-                f"supported values are {', '.join(_OPTUNA_NAMESPACES)}"
+                enum_error_message("namespace", namespace, _OPTUNA_NAMESPACES)
             )
 
     @staticmethod
@@ -5103,8 +5123,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         samplers, sampler = self.optuna_samplers_by_namespace(namespace)
         if sampler not in samplers:
             raise ValueError(
-                f"Invalid optuna {namespace} sampler value {sampler!r}: "
-                f"supported values are {', '.join(samplers)}"
+                enum_error_message(
+                    f"optuna {namespace} sampler", sampler, tuple(samplers)
+                )
             )
 
         try:
diff --git a/quickadapter/user_data/strategies/EnumErrors.py b/quickadapter/user_data/strategies/EnumErrors.py
new file mode 100644 (file)
index 0000000..da11d1f
--- /dev/null
@@ -0,0 +1,12 @@
+"""Canonical enum validation error message. Dependency-free (stdlib only).
+
+Owns the ``Invalid <ctx> value <value>: supported values are <options>`` form;
+messages that deviate from it (custom prefix/infix/suffix) are built inline.
+"""
+
+from collections.abc import Sequence
+from typing import Any
+
+
+def enum_error_message(ctx: str, value: Any, options: Sequence[str]) -> str:
+    return f"Invalid {ctx} value {value!r}: supported values are {', '.join(options)}"
index 9becce1ae7a1817c747125058b650394bb8e3306..2b2415c62bd0781e15d2ea5723419d7b6934c958 100644 (file)
@@ -20,6 +20,7 @@ from sklearn.preprocessing import (
     RobustScaler,
     StandardScaler,
 )
+from EnumErrors import enum_error_message
 
 logger = logging.getLogger(__name__)
 
@@ -422,10 +423,7 @@ class LabelTransformer(BaseTransform):
     ) -> NDArray[np.floating]:
         scaler_attr = family.registry.get(method)
         if scaler_attr is None:
-            raise ValueError(
-                f"Invalid {family.kind} value {method!r}: "
-                f"supported values are {', '.join(family.type_names)}"
-            )
+            raise ValueError(enum_error_message(family.kind, method, family.type_names))
         scaler = getattr(state, scaler_attr, None)
         if scaler is None:
             raise RuntimeError(f"{scaler_attr} not fitted")
@@ -514,8 +512,7 @@ class LabelTransformer(BaseTransform):
             return
 
         raise ValueError(
-            f"Invalid standardization value {method!r}: "
-            f"supported values are {', '.join(STANDARDIZATION_TYPES)}"
+            enum_error_message("standardization", method, STANDARDIZATION_TYPES)
         )
 
     def _fit_normalization(
@@ -536,8 +533,7 @@ class LabelTransformer(BaseTransform):
             return
 
         raise ValueError(
-            f"Invalid normalization value {method!r}: "
-            f"supported values are {', '.join(NORMALIZATION_TYPES)}"
+            enum_error_message("normalization", method, NORMALIZATION_TYPES)
         )
 
     def _fit_column(
index 9a3feb31e218e94c18656dcc35acb00d9f6d0caa..62c4f3b87861eb1b4896d5b3db388ce628192a7b 100644 (file)
@@ -59,6 +59,7 @@ from Utils import (
     compute_label_weight_known_at_lookahead,
     compute_label_weights,
     ensure_datetime_series,
+    enum_error_message,
     ewo,
     format_dict,
     format_number,
@@ -1349,8 +1350,11 @@ class QuickAdapterV3(IStrategy):
         )
         if trade_price_target_method_fn is None:
             raise ValueError(
-                f"Invalid trade_price_target_method value {self.trade_price_target_method!r}: "
-                f"supported values are {', '.join(TRADE_PRICE_TARGETS)}"
+                enum_error_message(
+                    "trade_price_target_method",
+                    self.trade_price_target_method,
+                    TRADE_PRICE_TARGETS,
+                )
             )
         return trade_price_target_method_fn()
 
@@ -1849,8 +1853,11 @@ class QuickAdapterV3(IStrategy):
             )
         else:
             raise ValueError(
-                f"Invalid interpolation_direction value {interpolation_direction!r}: "
-                f"supported values are {', '.join(QuickAdapterV3._INTERPOLATION_DIRECTIONS)}"
+                enum_error_message(
+                    "interpolation_direction",
+                    interpolation_direction,
+                    QuickAdapterV3._INTERPOLATION_DIRECTIONS,
+                )
             )
         candle_deviation = (
             candle_label_natr_value / 100.0
@@ -1920,7 +1927,7 @@ class QuickAdapterV3(IStrategy):
             candle_threshold = base_price * (1 - current_deviation)
         else:
             raise ValueError(
-                f"Invalid side value {side!r}: supported values are {', '.join(QuickAdapterV3._TRADE_DIRECTIONS)}"
+                enum_error_message("side", side, QuickAdapterV3._TRADE_DIRECTIONS)
             )
         self._candle_threshold_cache[cache_key] = candle_threshold
         return self._candle_threshold_cache[cache_key]
@@ -2344,8 +2351,9 @@ class QuickAdapterV3(IStrategy):
             return False
         else:
             raise ValueError(
-                f"Invalid trading_mode value {trading_mode!r}: "
-                f"supported values are {', '.join(QuickAdapterV3._TRADING_MODES)}"
+                enum_error_message(
+                    "trading_mode", trading_mode, QuickAdapterV3._TRADING_MODES
+                )
             )
 
     @cached_property
index 4cf3970807e808e3fb31ada5173abda16c033be3..19b97107b7eb6df7b5c7d051e265521a123eeb8b 100644 (file)
@@ -37,6 +37,7 @@ import pandas as pd
 import scipy as sp
 import talib.abstract as ta
 from freqtrade.misc import pair_to_filename
+from EnumErrors import enum_error_message
 from LabelTransformer import (
     COMBINED_AGGREGATIONS,
     COMBINED_METRICS,
@@ -1104,10 +1105,6 @@ def as_config_section(value: Any, name: str, logger: Logger) -> dict[str, Any]:
     return as_dict(value)
 
 
-def enum_error_message(ctx: str, value: Any, options: Sequence[str]) -> str:
-    return f"Invalid {ctx} value {value!r}: supported values are {', '.join(options)}"
-
-
 ValidateParamsFn = Callable[[dict[str, Any], Logger, str], dict[str, Any]]
 CrossFieldValidatorFn = Callable[[dict[str, Any], str], None]
 
@@ -2104,10 +2101,7 @@ def _calculate_coeffs(
     elif win_type == SMOOTHING_KERNELS[3]:  # "triang"
         coeffs = sp.signal.windows.triang(M=window, sym=True)
     else:
-        raise ValueError(
-            f"Invalid window type value {win_type!r}: "
-            f"supported values are {', '.join(SMOOTHING_KERNELS)}"
-        )
+        raise ValueError(enum_error_message("window type", win_type, SMOOTHING_KERNELS))
     normalized_coeffs = coeffs / np.sum(coeffs)
     normalized_coeffs.setflags(write=False)
     return normalized_coeffs
@@ -2372,8 +2366,7 @@ def _compute_pivot_sigmas(
         return np.full(M, float(sigma_candles), dtype=float)
     if bandwidth != FILL_BANDWIDTHS[1]:  # "knn"
         raise ValueError(
-            f"Invalid fill_bandwidth value {bandwidth!r}: "
-            f"supported values are {', '.join(FILL_BANDWIDTHS)}"
+            enum_error_message("fill_bandwidth", bandwidth, FILL_BANDWIDTHS)
         )
 
     d_k = _compute_pivot_kth_neighbor_distances(pivot_indices, neighbors)
@@ -3178,10 +3171,7 @@ def compute_label_weights(
             out=fill_weights,
         )
     else:
-        raise ValueError(
-            f"Invalid fill_method value {fill_method!r}: "
-            f"supported values are {', '.join(FILL_METHODS)}"
-        )
+        raise ValueError(enum_error_message("fill_method", fill_method, FILL_METHODS))
 
     return _scatter_weights(
         n_values=n_values,
@@ -4909,7 +4899,7 @@ def get_ngboost_dist(dist_name: str) -> type:
 
     if dist_name not in dist_map:
         raise ValueError(
-            f"Invalid dist_name {dist_name!r}: supported values are {', '.join(dist_map.keys())}"
+            enum_error_message("dist_name", dist_name, tuple(dist_map.keys()))
         )
 
     return dist_map[dist_name]
@@ -4952,10 +4942,7 @@ def get_refit_model_training_parameters(
         fitted_iterations = int(model.tree_count_)
         initial_iterations = 0
     else:
-        raise ValueError(
-            f"Invalid regressor value {regressor!r}: "
-            f"supported values are {', '.join(REGRESSORS)}"
-        )
+        raise ValueError(enum_error_message("regressor", regressor, REGRESSORS))
 
     spec = _REGRESSOR_SPEC_BY_NAME[regressor]
     # The sole caller refits the cold-started selection model
@@ -5015,10 +5002,7 @@ def fit_regressor(
 
     spec = _REGRESSOR_SPEC_BY_NAME.get(regressor)
     if spec is None:
-        raise ValueError(
-            f"Invalid regressor value {regressor!r}: "
-            f"supported values are {', '.join(REGRESSORS)}"
-        )
+        raise ValueError(enum_error_message("regressor", regressor, REGRESSORS))
     model_training_parameters.setdefault(spec.seed_param, 1)
     if trial is not None and vary_model_seed_by_trial:
         model_training_parameters[spec.seed_param] = (