From: Jérôme Benoit Date: Fri, 31 Jul 2026 13:07:07 +0000 (+0200) Subject: refactor(quickadapter): route inline enum error messages through enum_error_message... X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=64dc1e8a39ecb19af18d3b13b05d4cfa60ba6377;p=freqai-strategies.git refactor(quickadapter): route inline enum error messages through enum_error_message (#194) * 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 value : supported values are ' 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. --- diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 90f4d9e..afe00bd 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -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 index 0000000..da11d1f --- /dev/null +++ b/quickadapter/user_data/strategies/EnumErrors.py @@ -0,0 +1,12 @@ +"""Canonical enum validation error message. Dependency-free (stdlib only). + +Owns the ``Invalid value : supported values are `` 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)}" diff --git a/quickadapter/user_data/strategies/LabelTransformer.py b/quickadapter/user_data/strategies/LabelTransformer.py index 9becce1..2b2415c 100644 --- a/quickadapter/user_data/strategies/LabelTransformer.py +++ b/quickadapter/user_data/strategies/LabelTransformer.py @@ -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( diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 9a3feb3..62c4f3b 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -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 diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index 4cf3970..19b9710 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -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] = (