From: Jérôme Benoit Date: Fri, 26 Dec 2025 19:16:22 +0000 (+0100) Subject: refactor: harmonize errors and warnings messages X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=1ea5fcabc8fe507de1e33b5bef933a3e723767ff;p=freqai-strategies.git refactor: harmonize errors and warnings messages Signed-off-by: Jérôme Benoit --- diff --git a/ReforceXY/reward_space_analysis/reward_space_analysis.py b/ReforceXY/reward_space_analysis/reward_space_analysis.py index 414c99c..dd476a9 100644 --- a/ReforceXY/reward_space_analysis/reward_space_analysis.py +++ b/ReforceXY/reward_space_analysis/reward_space_analysis.py @@ -115,6 +115,14 @@ ALLOWED_EXIT_POTENTIAL_MODES = { "retain_previous", } +# Supported trading modes +TRADING_MODES: Tuple[str, ...] = ("spot", "margin", "futures") + +# Supported p-value adjustment methods +ADJUST_METHODS: Tuple[str, ...] = ("none", "benjamini_hochberg") +# Alias without underscore for convenience +_ADJUST_METHODS_ALIASES: frozenset[str] = frozenset({"benjaminihochberg"}) + DEFAULT_MODEL_REWARD_PARAMETERS: RewardParams = { "invalid_action": -2.0, @@ -305,7 +313,7 @@ def _to_bool(value: Any) -> bool: if text in {"false", "0", "no", "n", "off"}: return False # Unsupported type - raise ValueError(f"Unrecognized boolean literal: {value!r}") + raise ValueError(f"Param: unrecognized boolean literal {value!r}") def _get_bool_param(params: RewardParams, key: str, default: Optional[bool] = None) -> bool: @@ -422,7 +430,7 @@ def _clamp_float_to_bounds( if "min" in effective_bounds and adjusted < float(effective_bounds["min"]): if strict: raise ValueError( - f"Parameter '{key}'={adjusted} below min {float(effective_bounds['min'])}" + f"Param: '{key}'={adjusted} below min {float(effective_bounds['min'])}" ) adjusted = float(effective_bounds["min"]) reason_parts.append(f"min={float(effective_bounds['min'])}") @@ -430,14 +438,14 @@ def _clamp_float_to_bounds( if "max" in effective_bounds and adjusted > float(effective_bounds["max"]): if strict: raise ValueError( - f"Parameter '{key}'={adjusted} above max {float(effective_bounds['max'])}" + f"Param: '{key}'={adjusted} above max {float(effective_bounds['max'])}" ) adjusted = float(effective_bounds["max"]) reason_parts.append(f"max={float(effective_bounds['max'])}") if not np.isfinite(adjusted): if strict: - raise ValueError(f"Parameter '{key}' is non-finite: {adjusted}") + raise ValueError(f"Param: '{key}' is non-finite: {adjusted}") adjusted = float(effective_bounds.get("min", 0.0)) reason_parts.append("non_finite_reset") @@ -521,11 +529,13 @@ def _compute_duration_ratio(trade_duration: int, max_trade_duration_candles: int def _is_short_allowed(trading_mode: str) -> bool: mode = trading_mode.lower() - if mode in {"margin", "futures"}: + if mode in TRADING_MODES[1:]: # "margin", "futures" return True - if mode == "spot": + if mode == TRADING_MODES[0]: # "spot" return False - raise ValueError("Unsupported trading mode. Expected one of: spot, margin, futures") + raise ValueError( + f"Config: unsupported trading mode '{mode}'. Expected one of: {list(TRADING_MODES)}" + ) def _fail_safely(reason: str) -> float: @@ -619,7 +629,7 @@ def validate_reward_parameters( } continue if strict: - raise ValueError(f"Parameter '{key}' is non-numeric or invalid: {original_val!r}") + raise ValueError(f"Param: '{key}' is non-numeric or invalid: {original_val!r}") adjusted = bounds.get("min", 0.0) sanitized[key] = adjusted adjustments[key] = { @@ -1451,7 +1461,7 @@ def parse_overrides(overrides: Iterable[str]) -> RewardParams: parsed: RewardParams = {} for override in overrides: if "=" not in override: - raise ValueError(f"Invalid override format: '{override}'") + raise ValueError(f"CLI: invalid override format '{override}'. Expected 'key=value'") key, value = override.split("=", 1) try: parsed[key] = float(value) @@ -1807,7 +1817,7 @@ def _binned_stats( """Return count/mean/std/min/max of target grouped by clipped bins of column.""" bins_arr = np.asarray(list(bins), dtype=float) if bins_arr.ndim != 1 or bins_arr.size < 2: - raise ValueError("bins must contain at least two edges") + raise ValueError("Stats: bins must contain at least two edges") clipped = df[column].clip(lower=float(bins_arr[0]), upper=float(bins_arr[-1])) categories = pd.cut( clipped, @@ -1991,7 +2001,7 @@ def _perform_feature_analysis( or permutation_importance is None or r2_score is None ): - raise ImportError("scikit-learn is not available; skipping feature analysis.") + raise ImportError("Feature analysis: scikit-learn is not available") canonical_features = [ "pnl", @@ -2172,7 +2182,7 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr with path.open("rb") as f: episodes_data = pickle.load(f) except Exception as e: - raise ValueError(f"Failed to unpickle '{path}': {e!r}") from e + raise ValueError(f"Data: failed to unpickle '{path}': {e!r}") from e # Top-level dict with 'transitions' if isinstance(episodes_data, dict) and "transitions" in episodes_data: @@ -2184,11 +2194,11 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr df = pd.DataFrame(list(candidate)) except TypeError: raise ValueError( - f"Top-level 'transitions' in '{path}' is not iterable (type {type(candidate)!r})." + f"Data: 'transitions' in '{path}' is not iterable (type {type(candidate)!r})" ) except Exception as e: raise ValueError( - f"Could not build DataFrame from top-level 'transitions' in '{path}': {e!r}" + f"Data: could not build DataFrame from 'transitions' in '{path}': {e!r}" ) from e # List of episodes where some entries have 'transitions' elif isinstance(episodes_data, list) and any( @@ -2206,7 +2216,7 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr all_transitions.extend(list(trans)) except TypeError: raise ValueError( - f"Episode 'transitions' is not iterable in file '{path}'; found type {type(trans)!r}" + f"Data: episode 'transitions' is not iterable in '{path}' (type {type(trans)!r})" ) else: skipped += 1 @@ -2220,7 +2230,7 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr df = pd.DataFrame(all_transitions) except Exception as e: raise ValueError( - f"Could not build DataFrame from flattened transitions in '{path}': {e!r}" + f"Data: could not build DataFrame from transitions in '{path}': {e!r}" ) from e else: try: @@ -2230,7 +2240,7 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr df = pd.DataFrame(episodes_data) except Exception as e: raise ValueError( - f"Could not convert pickled object from '{path}' to DataFrame: {e!r}" + f"Data: could not convert pickled object from '{path}' to DataFrame: {e!r}" ) from e # Coerce common numeric fields; warn when values are coerced to NaN @@ -2287,8 +2297,8 @@ def load_real_episodes(path: Path, *, enforce_columns: bool = True) -> pd.DataFr if missing_required: if enforce_columns: raise ValueError( - f"Loaded episodes data is missing required columns: {sorted(missing_required)}. " - f"Found columns: {sorted(list(df.columns))}." + f"Data: missing required columns {sorted(missing_required)}. " + f"Found: {sorted(list(df.columns))}" ) warnings.warn( f"Missing columns {sorted(missing_required)}; filled with NaN when loading (enforce_columns=False)", @@ -2416,7 +2426,7 @@ def _validate_distribution_metrics(metrics: Dict[str, float]) -> None: def statistical_hypothesis_tests( - df: pd.DataFrame, *, adjust_method: str = "none", seed: int = 42 + df: pd.DataFrame, *, adjust_method: str = ADJUST_METHODS[0], seed: int = 42 ) -> Dict[str, Any]: """Statistical hypothesis tests (Spearman, Kruskal-Wallis, Mann-Whitney). @@ -2517,11 +2527,13 @@ def statistical_hypothesis_tests( } # Optional multiple testing correction (Benjamini-Hochberg) - if adjust_method not in {"none", "benjamini_hochberg", "benjaminihochberg"}: + _valid_adjust = set(ADJUST_METHODS) | _ADJUST_METHODS_ALIASES + if adjust_method not in _valid_adjust: raise ValueError( - "Unsupported adjust_method. Use 'none', 'benjamini_hochberg', or 'benjaminihochberg'." + f"Stats: unsupported adjust_method '{adjust_method}'. " + f"Expected one of: {list(ADJUST_METHODS)}" ) - if adjust_method in {"benjamini_hochberg", "benjaminihochberg"} and results: + if adjust_method in _valid_adjust - {ADJUST_METHODS[0]} and results: # Collect p-values items = list(results.items()) pvals = np.array([v[1]["p_value"] for v in items]) @@ -3447,8 +3459,8 @@ def build_argument_parser() -> argparse.ArgumentParser: parser.add_argument( "--trading_mode", type=str.lower, - choices=["spot", "margin", "futures"], - default="spot", + choices=list(TRADING_MODES), + default=TRADING_MODES[0], help=("Trading mode to simulate (spot disables shorts). Default: spot."), ) parser.add_argument( @@ -3482,8 +3494,8 @@ def build_argument_parser() -> argparse.ArgumentParser: parser.add_argument( "--pvalue_adjust", type=str.lower, - choices=["none", "benjamini_hochberg"], - default="none", + choices=list(ADJUST_METHODS), + default=ADJUST_METHODS[0], help="Multiple testing correction method for hypothesis tests (default: none).", ) parser.add_argument( @@ -3527,7 +3539,7 @@ def write_complete_statistical_analysis( seed: int, real_df: Optional[pd.DataFrame] = None, *, - adjust_method: str = "none", + adjust_method: str = ADJUST_METHODS[0], stats_seed: Optional[int] = None, strict_diagnostics: bool = False, bootstrap_resamples: int = 10000, diff --git a/ReforceXY/user_data/freqaimodels/ReforceXY.py b/ReforceXY/user_data/freqaimodels/ReforceXY.py index 18b66ac..83c3ea6 100644 --- a/ReforceXY/user_data/freqaimodels/ReforceXY.py +++ b/ReforceXY/user_data/freqaimodels/ReforceXY.py @@ -267,7 +267,8 @@ class ReforceXY(BaseReinforcementLearningModel): self.pairs: List[str] = self.config.get("exchange", {}).get("pair_whitelist") if not self.pairs: raise ValueError( - "FreqAI model requires StaticPairList method defined in pairlists configuration and pair_whitelist defined in exchange section configuration" + "Config: missing 'pair_whitelist' in exchange section " + "or StaticPairList method not defined in pairlists configuration" ) self.action_masking: bool = ( self.model_type == ReforceXY._MODEL_TYPES[2] @@ -813,7 +814,7 @@ class ReforceXY(BaseReinforcementLearningModel): train_df = data_dictionary.get("train_features") train_timesteps = len(train_df) if train_timesteps <= 0: - raise ValueError("train_features dataframe has zero length") + raise ValueError("Training: train_features dataframe has zero length") test_df = data_dictionary.get("test_features") eval_timesteps = len(test_df) train_cycles = max(1, int(self.rl_config.get("train_cycles", 25))) @@ -1172,7 +1173,8 @@ class ReforceXY(BaseReinforcementLearningModel): ) else: raise ValueError( - f"Unsupported storage backend: {storage_backend}. Supported backends are: {', '.join(ReforceXY._STORAGE_BACKENDS)}" + f"Hyperopt: unsupported storage backend '{storage_backend}'. " + f"Expected one of: {list(ReforceXY._STORAGE_BACKENDS)}" ) return storage @@ -1213,7 +1215,8 @@ class ReforceXY(BaseReinforcementLearningModel): ) else: raise ValueError( - f"Unsupported sampler: {sampler}. Supported samplers: {', '.join(ReforceXY._SAMPLER_TYPES)}" + f"Hyperopt: unsupported sampler '{sampler}'. " + f"Expected one of: {list(ReforceXY._SAMPLER_TYPES)}" ) @staticmethod @@ -1547,7 +1550,9 @@ class ReforceXY(BaseReinforcementLearningModel): elif ReforceXY._MODEL_TYPES[3] in self.model_type: return sample_params_dqn(trial) else: - raise NotImplementedError(f"{self.model_type} not supported for hyperopt") + raise NotImplementedError( + f"Hyperopt: model type '{self.model_type}' not supported" + ) def objective( self, trial: Trial, dk: FreqaiDataKitchen, total_timesteps: int @@ -4322,7 +4327,7 @@ def convert_optuna_params_to_model_params( lr = optuna_params.get("learning_rate") if lr is None: - raise ValueError(f"missing {'learning_rate'} in optuna params for {model_type}") + raise ValueError(f"Optuna: missing 'learning_rate' in params for {model_type}") lr = get_schedule( optuna_params.get("lr_schedule", ReforceXY._SCHEDULE_TYPES[1]), float(lr) ) # default: "constant" @@ -4341,7 +4346,9 @@ def convert_optuna_params_to_model_params( ] for param in required_ppo_params: if optuna_params.get(param) is None: - raise ValueError(f"missing '{param}' in optuna params for {model_type}") + raise ValueError( + f"Optuna: missing '{param}' in params for {model_type}" + ) cr = optuna_params.get("clip_range") cr = get_schedule( optuna_params.get("cr_schedule", ReforceXY._SCHEDULE_TYPES[1]), @@ -4384,7 +4391,9 @@ def convert_optuna_params_to_model_params( ] for param in required_dqn_params: if optuna_params.get(param) is None: - raise ValueError(f"missing '{param}' in optuna params for {model_type}") + raise ValueError( + f"Optuna: missing '{param}' in params for {model_type}" + ) train_freq = optuna_params.get("train_freq") subsample_steps = optuna_params.get("subsample_steps") gradient_steps = compute_gradient_steps(train_freq, subsample_steps) @@ -4418,7 +4427,7 @@ def convert_optuna_params_to_model_params( ): # "QRDQN" policy_kwargs["n_quantiles"] = int(optuna_params["n_quantiles"]) else: - raise ValueError(f"Model {model_type} not supported") + raise ValueError(f"Optuna: model type '{model_type}' not supported") if optuna_params.get("net_arch"): net_arch_value = str(optuna_params["net_arch"]) diff --git a/ReforceXY/user_data/strategies/RLAgentStrategy.py b/ReforceXY/user_data/strategies/RLAgentStrategy.py index e3bc5ff..3a0db9f 100644 --- a/ReforceXY/user_data/strategies/RLAgentStrategy.py +++ b/ReforceXY/user_data/strategies/RLAgentStrategy.py @@ -168,4 +168,7 @@ class RLAgentStrategy(IStrategy): elif trading_mode == RLAgentStrategy._TRADING_MODES[2]: return False else: - raise ValueError(f"Invalid trading_mode: {trading_mode}") + raise ValueError( + f"Config: invalid trading_mode '{trading_mode}'. " + f"Expected one of: {list(RLAgentStrategy._TRADING_MODES)}" + ) diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 7591d0e..4d7122d 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -42,6 +42,7 @@ from Utils import ( ExtremaSelectionMethod = Literal["rank_extrema", "rank_peaks", "partition"] OptunaNamespace = Literal["hp", "train", "label"] +ClusterSelectionMethod = Literal["medoid", "min"] CustomThresholdMethod = Literal["median", "soft_extremum"] SkimageThresholdMethod = Literal[ "mean", "isodata", "li", "minimum", "otsu", "triangle", "yen" @@ -101,15 +102,20 @@ class QuickAdapterRegressorV3(BaseRegressionModel): *_CUSTOM_THRESHOLD_METHODS, ) - _OPTUNA_STORAGE_BACKENDS: Final[tuple[str, ...]] = ("file", "sqlite") - _OPTUNA_SAMPLERS: Final[tuple[str, ...]] = ("tpe", "auto") - _OPTUNA_NAMESPACES: Final[tuple[OptunaNamespace, ...]] = ("hp", "train", "label") + _CLUSTER_SELECTION_METHODS: Final[tuple[ClusterSelectionMethod, ...]] = ( + "medoid", + "min", + ) _OPTUNA_LABEL_N_OBJECTIVES: Final[int] = 7 _OPTUNA_LABEL_DIRECTIONS: Final[tuple[optuna.study.StudyDirection, ...]] = ( optuna.study.StudyDirection.MAXIMIZE, ) * _OPTUNA_LABEL_N_OBJECTIVES + _OPTUNA_STORAGE_BACKENDS: Final[tuple[str, ...]] = ("file", "sqlite") + _OPTUNA_SAMPLERS: Final[tuple[str, ...]] = ("tpe", "auto") + _OPTUNA_NAMESPACES: Final[tuple[OptunaNamespace, ...]] = ("hp", "train", "label") + _SCIPY_METRICS: Final[tuple[str, ...]] = ( # "braycurtis", # "canberra", @@ -160,6 +166,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel): *_CUSTOM_METRICS, ) + _UNSUPPORTED_CLUSTER_METRICS: Final[tuple[str, ...]] = ( + "mahalanobis", + "seuclidean", + "jensenshannon", + ) + PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT: Final[float] = 0.999 PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT: Final[float] = 12.0 PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT: Final[float] = 1.0 @@ -205,6 +217,14 @@ class QuickAdapterRegressorV3(BaseRegressionModel): def _metrics_set() -> set[str]: return set(QuickAdapterRegressorV3._METRICS) + @staticmethod + def _unsupported_cluster_metrics_set() -> set[str]: + return set(QuickAdapterRegressorV3._UNSUPPORTED_CLUSTER_METRICS) + + @staticmethod + def _cluster_selection_methods_set() -> set[ClusterSelectionMethod]: + return set(QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS) + @staticmethod def _get_label_p_order_default(metric: str) -> Optional[float]: if metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]: # "minkowski" @@ -646,7 +666,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): QuickAdapterRegressorV3._CUSTOM_METRICS[10], # "kmeans2" }: logger.info( - f" label_kmeans_selection: min (default for {label_metric})" + f" label_kmeans_selection: {QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[1]} (default for {label_metric})" ) label_kmedoids_metric_config = self.ft_params.get("label_kmedoids_metric") if label_kmedoids_metric_config is not None: @@ -669,7 +689,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[11] ): # "kmedoids" logger.info( - f" label_kmedoids_selection: min (default for {label_metric})" + f" label_kmedoids_selection: {QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[1]} (default for {label_metric})" ) label_knn_metric_config = self.ft_params.get("label_knn_metric") @@ -779,8 +799,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): params = self._optuna_label_params.get(pair) else: raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES)}" + f"Invalid namespace '{namespace}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES)}" ) return params @@ -795,8 +815,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): self._optuna_label_params[pair] = params else: raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES)}" + f"Invalid namespace '{namespace}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES)}" ) def get_optuna_value(self, pair: str, namespace: str) -> float: @@ -806,8 +826,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): value = self._optuna_train_value.get(pair) else: raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES[:2])}" # Only hp and train + f"Invalid namespace '{namespace}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES[:2])}" # Only hp and train ) return value @@ -818,8 +838,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): self._optuna_train_value[pair] = value else: raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES[:2])}" # Only hp and train + f"Invalid namespace '{namespace}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_NAMESPACES[:2])}" # Only hp and train ) def get_optuna_values(self, pair: str, namespace: str) -> list[float | int]: @@ -827,8 +847,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): values = self._optuna_label_values.get(pair) else: raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label + f"Invalid namespace '{namespace}'. " + f"Supported: {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label ) return values @@ -839,8 +859,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): self._optuna_label_values[pair] = values else: raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label + f"Invalid namespace '{namespace}'. " + f"Supported: {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label ) def init_optuna_label_candle_pool(self) -> None: @@ -1036,11 +1056,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel): QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2] }: # Only "label" raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label + f"Invalid namespace '{namespace}'. " + f"Supported: {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label ) if not callable(callback): - raise ValueError("callback must be callable") + raise ValueError("Invalid callback: must be callable") self._optuna_label_candles[pair] += 1 if pair not in self._optuna_label_incremented_pairs: self._optuna_label_incremented_pairs.append(pair) @@ -1371,8 +1391,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): pred_minima = pred_extrema[pred_extrema < -eps] else: raise ValueError( - f"Unsupported extrema selection method: {extrema_selection}. " - f"Supported methods are {', '.join(QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS)}" + f"Invalid extrema_selection '{extrema_selection}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS)}" ) return pred_minima, pred_maxima @@ -1413,7 +1433,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): extrema_fraction: float = 1.0, ) -> tuple[float, float]: if alpha < 0: - raise ValueError("alpha must be non-negative") + raise ValueError(f"Invalid alpha {alpha}: must be >= 0") pred_minima, pred_maxima = QuickAdapterRegressorV3.get_pred_min_max( pred_extrema, extrema_selection, extrema_fraction ) @@ -1465,7 +1485,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): try: threshold_func = getattr(skimage.filters, f"threshold_{method}") except AttributeError: - raise ValueError(f"Unknown skimage threshold function: threshold_{method}") + raise ValueError( + f"Invalid skimage threshold method '{method}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._SKIMAGE_THRESHOLD_METHODS)}" + ) min_func = QuickAdapterRegressorV3.apply_skimage_threshold max_func = QuickAdapterRegressorV3.apply_skimage_threshold @@ -1521,7 +1544,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): Must contain only finite values (no NaN or inf). - metric: distance metric name accepted by scipy.spatial.distance.pdist. - weights: optional weight vector per feature (passed as 'w' to pdist). - Not supported by mahalanobis, seuclidean, jensenshannon. + Not supported by metrics in _UNSUPPORTED_CLUSTER_METRICS. Must have size equal to n_features and contain finite non-negative values. - p: optional Minkowski order (default 2.0 if metric=='minkowski'). @@ -1542,26 +1565,26 @@ class QuickAdapterRegressorV3(BaseRegressionModel): array([2. , 2.41421356, 2.41421356]) """ if matrix.ndim != 2: - raise ValueError("matrix must be 2-dimensional") + raise ValueError("Invalid matrix: must be 2-dimensional") if matrix.shape[1] == 0: - raise ValueError("matrix must have at least one feature") + raise ValueError("Invalid matrix: must have at least one feature") if not np.all(np.isfinite(matrix)): - raise ValueError("matrix must contain only finite values (no NaN or inf)") + raise ValueError( + "Invalid matrix: must contain only finite values (no NaN or inf)" + ) if weights is not None: if weights.size != matrix.shape[1]: raise ValueError( - f"weights size {weights.size} must match number of features {matrix.shape[1]}" + f"Invalid weights: size {weights.size} must match number of features {matrix.shape[1]}" ) if not np.all(np.isfinite(weights)) or np.any(weights < 0): - raise ValueError("weights must be finite and non-negative") - if metric in { - QuickAdapterRegressorV3._SCIPY_METRICS[4], # "mahalanobis" - QuickAdapterRegressorV3._SCIPY_METRICS[6], # "seuclidean" - QuickAdapterRegressorV3._SCIPY_METRICS[3], # "jensenshannon" - }: - raise ValueError(f"weights not supported for metric '{metric}'") + raise ValueError("Invalid weights: must be finite and non-negative") + if metric in QuickAdapterRegressorV3._unsupported_cluster_metrics_set(): + raise ValueError( + f"Invalid weights: not supported for metric '{metric}'" + ) matrix = np.asarray(matrix, dtype=np.float64) if weights is not None: @@ -1601,17 +1624,17 @@ class QuickAdapterRegressorV3(BaseRegressionModel): directions: list[optuna.study.StudyDirection], ) -> NDArray[np.floating]: if objective_values_matrix.ndim != 2: - raise ValueError("objective_values_matrix must be 2-dimensional") + raise ValueError("Invalid objective_values_matrix: must be 2-dimensional") n_samples, n_objectives = objective_values_matrix.shape if n_samples == 0 or n_objectives == 0: raise ValueError( - "objective_values_matrix must have at least one sample and one objective" + "Invalid objective_values_matrix: must have at least one sample and one objective" ) if len(directions) != n_objectives: raise ValueError( - f"Number of directions ({len(directions)}) must match number of objectives ({n_objectives})" + f"Invalid directions: length ({len(directions)}) must match number of objectives ({n_objectives})" ) normalized_matrix = np.zeros_like(objective_values_matrix, dtype=float) @@ -1699,16 +1722,16 @@ class QuickAdapterRegressorV3(BaseRegressionModel): metrics: set[str], ) -> NDArray[np.floating]: if normalized_matrix.ndim != 2: - raise ValueError("normalized_matrix must be 2-dimensional") + raise ValueError("Invalid normalized_matrix: must be 2-dimensional") n_objectives = normalized_matrix.shape[1] n_samples = normalized_matrix.shape[0] if n_samples == 0 or n_objectives == 0: raise ValueError( - "normalized_matrix must have at least one sample and one objective" + "Invalid normalized_matrix: must have at least one sample and one objective" ) if not np.all(np.isfinite(normalized_matrix)): raise ValueError( - "normalized_matrix must contain only finite values (no NaN or inf)" + "Invalid normalized_matrix: must contain only finite values (no NaN or inf)" ) label_p_order = self.ft_params.get("label_p_order") label_weights = self.ft_params.get("label_weights") @@ -1718,17 +1741,19 @@ class QuickAdapterRegressorV3(BaseRegressionModel): np_weights = np.array(label_weights, dtype=float) else: raise ValueError( - f"label_weights must be a list, tuple, or array, got {type(label_weights).__name__}" + f"Invalid label_weights: must be a list, tuple, or array, got {type(label_weights).__name__}" ) if np_weights.size != n_objectives: - raise ValueError("label_weights length must match number of objectives") + raise ValueError( + "Invalid label_weights: length must match number of objectives" + ) if not np.all(np.isfinite(np_weights)): - raise ValueError("label_weights must contain only finite values") + raise ValueError("Invalid label_weights: must contain only finite values") if np.any(np_weights < 0): - raise ValueError("label_weights values must be non-negative") + raise ValueError("Invalid label_weights: values must be non-negative") label_weights_sum = np.nansum(np.abs(np_weights)) if np.isclose(label_weights_sum, 0.0): - raise ValueError("label_weights sum cannot be zero") + raise ValueError("Invalid label_weights: sum cannot be zero") np_weights = np_weights / label_weights_sum ideal_point = np.ones(n_objectives) @@ -1751,11 +1776,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): if metric in QuickAdapterRegressorV3._scipy_metrics_set(): cdist_kwargs: dict[str, Any] = {} - if metric not in { - QuickAdapterRegressorV3._SCIPY_METRICS[4], # "mahalanobis" - QuickAdapterRegressorV3._SCIPY_METRICS[6], # "seuclidean" - QuickAdapterRegressorV3._SCIPY_METRICS[3], # "jensenshannon" - }: + if metric not in QuickAdapterRegressorV3._unsupported_cluster_metrics_set(): cdist_kwargs["w"] = np_weights if metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]: # "minkowski" cdist_kwargs["p"] = ( @@ -1778,7 +1799,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): variances = np.nanvar(np_sqrt_normalized_matrix, axis=0, ddof=1) if np.any(variances <= 0): raise ValueError( - "shellinger metric requires non-zero variance for all objectives" + "Invalid data for shellinger metric: requires non-zero variance for all objectives" ) np_weights = 1 / variances return ( @@ -1821,13 +1842,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "label_medoid_metric", QuickAdapterRegressorV3._SCIPY_METRICS[2], # "euclidean" ) - if label_medoid_metric in { - QuickAdapterRegressorV3._SCIPY_METRICS[4], # "mahalanobis" - QuickAdapterRegressorV3._SCIPY_METRICS[6], # "seuclidean" - QuickAdapterRegressorV3._SCIPY_METRICS[3], # "jensenshannon" - }: + if ( + label_medoid_metric + in QuickAdapterRegressorV3._unsupported_cluster_metrics_set() + ): raise ValueError( - f"Unsupported label_medoid_metric: {label_medoid_metric}. Supported are euclidean/minkowski/cityblock/chebyshev/..." + f"Invalid label_medoid_metric '{label_medoid_metric}'. " + f"Unsupported: {', '.join(QuickAdapterRegressorV3._UNSUPPORTED_CLUSTER_METRICS)}" ) p = None if ( @@ -1864,13 +1885,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "label_kmeans_metric", QuickAdapterRegressorV3._SCIPY_METRICS[2], # "euclidean" ) - if label_kmeans_metric in { - QuickAdapterRegressorV3._SCIPY_METRICS[4], # "mahalanobis" - QuickAdapterRegressorV3._SCIPY_METRICS[6], # "seuclidean" - QuickAdapterRegressorV3._SCIPY_METRICS[3], # "jensenshannon" - }: + if ( + label_kmeans_metric + in QuickAdapterRegressorV3._unsupported_cluster_metrics_set() + ): raise ValueError( - f"Unsupported label_kmeans_metric: {label_kmeans_metric}. Supported are euclidean/minkowski/cityblock/chebyshev/..." + f"Invalid label_kmeans_metric '{label_kmeans_metric}'. " + f"Unsupported: {', '.join(QuickAdapterRegressorV3._UNSUPPORTED_CLUSTER_METRICS)}" ) cdist_kwargs: dict[str, Any] = {} if ( @@ -1888,7 +1909,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): metric=label_kmeans_metric, **cdist_kwargs, ).flatten() - label_kmeans_selection = self.ft_params.get("label_kmeans_selection", "min") + label_kmeans_selection = self.ft_params.get( + "label_kmeans_selection", + QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[1], # "min" + ) ordered_cluster_indices = np.argsort(cluster_center_distances_to_ideal) best_cluster_indices = None for cluster_index in ordered_cluster_indices: @@ -1900,7 +1924,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): if best_cluster_indices is not None and best_cluster_indices.size > 0: if ( label_kmeans_selection - == QuickAdapterRegressorV3._CUSTOM_METRICS[16] # "medoid" + == QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[0] # "medoid" ): p = None if ( @@ -1927,7 +1951,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel): **cdist_kwargs, ).item() trial_distances[best_trial_index] = best_trial_distance - elif label_kmeans_selection == "min": + elif ( + label_kmeans_selection + == QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[1] # "min" + ): best_cluster_distances = sp.spatial.distance.cdist( normalized_matrix[best_cluster_indices], ideal_point_2d, @@ -1941,7 +1968,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ] else: raise ValueError( - f"Unsupported label_kmeans_selection: {label_kmeans_selection}. Supported are medoid/min" + f"Invalid label_kmeans_selection '{label_kmeans_selection}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS)}" ) return trial_distances elif metric == QuickAdapterRegressorV3._CUSTOM_METRICS[11]: # "kmedoids" @@ -1950,13 +1978,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "label_kmedoids_metric", QuickAdapterRegressorV3._SCIPY_METRICS[2], # "euclidean" ) - if label_kmedoids_metric in { - QuickAdapterRegressorV3._SCIPY_METRICS[4], # "mahalanobis" - QuickAdapterRegressorV3._SCIPY_METRICS[6], # "seuclidean" - QuickAdapterRegressorV3._SCIPY_METRICS[3], # "jensenshannon" - }: + if ( + label_kmedoids_metric + in QuickAdapterRegressorV3._unsupported_cluster_metrics_set() + ): raise ValueError( - f"Unsupported label_kmedoids_metric: {label_kmedoids_metric}. Supported are euclidean/minkowski/cityblock/chebyshev/..." + f"Invalid label_kmedoids_metric '{label_kmedoids_metric}'. " + f"Unsupported: {', '.join(QuickAdapterRegressorV3._UNSUPPORTED_CLUSTER_METRICS)}" ) kmedoids_kwargs: dict[str, Any] = { "metric": label_kmedoids_metric, @@ -1984,7 +2012,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): **cdist_kwargs, ).flatten() label_kmedoids_selection = self.ft_params.get( - "label_kmedoids_selection", "min" + "label_kmedoids_selection", + QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[1], # "min" ) best_medoid_distance_position = np.nanargmin(medoid_distances_to_ideal) best_medoid_index = medoid_indices[best_medoid_distance_position] @@ -1994,12 +2023,15 @@ class QuickAdapterRegressorV3(BaseRegressionModel): if best_cluster_indices.size > 0: if ( label_kmedoids_selection - == QuickAdapterRegressorV3._CUSTOM_METRICS[16] # "medoid" + == QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[0] # "medoid" ): trial_distances[best_medoid_index] = medoid_distances_to_ideal[ best_medoid_distance_position ] - elif label_kmedoids_selection == "min": + elif ( + label_kmedoids_selection + == QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS[1] # "min" + ): if best_cluster_indices.size == 1: best_trial_index = best_cluster_indices[0] trial_distances[best_trial_index] = medoid_distances_to_ideal[ @@ -2019,7 +2051,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ] else: raise ValueError( - f"Unsupported label_kmedoids_selection: {label_kmedoids_selection}. Supported are medoid/min" + f"Invalid label_kmedoids_selection '{label_kmedoids_selection}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._CLUSTER_SELECTION_METHODS)}" ) return trial_distances elif metric in { @@ -2087,7 +2120,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return np.nanmax(neighbor_distances, axis=1) else: raise ValueError( - f"Unsupported label metric: {metric}. Supported metrics are {', '.join(metrics)}" + f"Invalid label metric '{metric}'. Supported: {', '.join(metrics)}" ) def _get_multi_objective_study_best_trial( @@ -2097,8 +2130,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2] }: # Only "label" raise ValueError( - f"Invalid namespace: {namespace}. " - f"Expected {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label + f"Invalid namespace '{namespace}'. " + f"Supported: {QuickAdapterRegressorV3._OPTUNA_NAMESPACES[2]}" # Only label ) n_objectives = len(study.directions) if n_objectives < 2: @@ -2114,7 +2147,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) # "euclidean" if label_metric not in metrics: raise ValueError( - f"Unsupported label metric: {label_metric}. Supported metrics are {', '.join(metrics)}" + f"Invalid label_metric '{label_metric}'. Supported: {', '.join(metrics)}" ) best_trials = [ @@ -2284,8 +2317,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) else: raise ValueError( - f"Unsupported optuna storage backend: {storage_backend}. " - f"Supported backends are {', '.join(QuickAdapterRegressorV3._OPTUNA_STORAGE_BACKENDS)}" + f"Invalid optuna storage_backend '{storage_backend}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_STORAGE_BACKENDS)}" ) return storage @@ -2316,8 +2349,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) else: raise ValueError( - f"Unsupported sampler: {sampler}. " - f"Supported samplers are {', '.join(QuickAdapterRegressorV3._OPTUNA_SAMPLERS)}" + f"Invalid optuna sampler '{sampler}'. " + f"Supported: {', '.join(QuickAdapterRegressorV3._OPTUNA_SAMPLERS)}" ) def optuna_create_study( diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index b086144..68c73c4 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -727,7 +727,7 @@ class QuickAdapterV3(IStrategy): def get_label_natr_ratio_percent(self, pair: str, percent: float) -> float: if not isinstance(percent, float) or not (0.0 <= percent <= 1.0): raise ValueError( - f"Invalid percent value: {percent}. It should be a float between 0 and 1" + f"Invalid percent {percent}: must be a float between 0 and 1" ) return self.get_label_natr_ratio(pair) * percent @@ -1416,7 +1416,8 @@ class QuickAdapterV3(IStrategy): trade_price_target_fn = trade_price_target_methods.get(trade_price_target) if trade_price_target_fn is None: raise ValueError( - f"Invalid trade_price_target: {trade_price_target}. Available: {', '.join(sorted(TRADE_PRICE_TARGETS))}" + f"Invalid trade_price_target '{trade_price_target}'. " + f"Supported: {', '.join(sorted(TRADE_PRICE_TARGETS))}" ) return trade_price_target_fn() @@ -1445,7 +1446,7 @@ class QuickAdapterV3(IStrategy): ) -> Optional[float]: if not (0.0 <= natr_ratio_percent <= 1.0): raise ValueError( - f"natr_ratio_percent must be in [0, 1], got {natr_ratio_percent}" + f"Invalid natr_ratio_percent {natr_ratio_percent}: must be in [0, 1]" ) trade_duration_candles = self.get_trade_duration_candles(df, trade) if not QuickAdapterV3.is_trade_duration_valid(trade_duration_candles): @@ -1472,7 +1473,7 @@ class QuickAdapterV3(IStrategy): ) -> Optional[float]: if not (0.0 <= natr_ratio_percent <= 1.0): raise ValueError( - f"natr_ratio_percent must be in [0, 1], got {natr_ratio_percent}" + f"Invalid natr_ratio_percent {natr_ratio_percent}: must be in [0, 1]" ) trade_duration_candles = self.get_trade_duration_candles(df, trade) if not QuickAdapterV3.is_trade_duration_valid(trade_duration_candles): @@ -1494,7 +1495,7 @@ class QuickAdapterV3(IStrategy): callback: Callable[[], None], ) -> None: if not callable(callback): - raise ValueError("callback must be callable") + raise ValueError("Invalid callback: must be callable") timestamp = int(current_time.timestamp()) candle_duration_secs = max(1, int(self._candle_duration_secs)) candle_start_secs = (timestamp // candle_duration_secs) * candle_duration_secs @@ -1818,8 +1819,8 @@ class QuickAdapterV3(IStrategy): ) else: raise ValueError( - f"Invalid interpolation_direction: {interpolation_direction}. " - f"Expected {', '.join(QuickAdapterV3._INTERPOLATION_DIRECTIONS)}" + f"Invalid interpolation_direction '{interpolation_direction}'. " + f"Supported: {', '.join(QuickAdapterV3._INTERPOLATION_DIRECTIONS)}" ) candle_deviation = ( candle_label_natr_value / 100.0 @@ -1892,7 +1893,7 @@ class QuickAdapterV3(IStrategy): candle_threshold = base_price * (1 - current_deviation) else: raise ValueError( - f"Invalid side: {side}. Expected {', '.join(QuickAdapterV3._TRADE_DIRECTIONS)}" + f"Invalid side '{side}'. Supported: {', '.join(QuickAdapterV3._TRADE_DIRECTIONS)}" ) self._candle_threshold_cache[cache_key] = candle_threshold return self._candle_threshold_cache[cache_key] @@ -2475,8 +2476,8 @@ class QuickAdapterV3(IStrategy): return False else: raise ValueError( - f"Invalid trading_mode: {trading_mode}. " - f"Expected {', '.join(QuickAdapterV3._TRADING_MODES)}" + f"Invalid trading_mode '{trading_mode}'. " + f"Supported: {', '.join(QuickAdapterV3._TRADING_MODES)}" ) def leverage( diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index 8e194f9..b6b6f7b 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -199,7 +199,7 @@ def non_zero_diff(s1: pd.Series, s2: pd.Series) -> pd.Series: @lru_cache(maxsize=8) def get_odd_window(window: int) -> int: if window < 1: - raise ValueError("Window size must be greater than 0") + raise ValueError(f"Invalid window {window}: must be > 0") return window if window % 2 == 1 else window + 1 @@ -232,7 +232,10 @@ def _calculate_coeffs( elif win_type == SMOOTHING_METHODS[2]: # "triang" coeffs = sp.signal.windows.triang(M=window, sym=True) else: - raise ValueError(f"Unknown window type: {win_type}") + raise ValueError( + f"Invalid window type '{win_type}'. " + f"Supported: {', '.join(SMOOTHING_METHODS[:3])}" + ) return coeffs / np.sum(coeffs) @@ -439,7 +442,10 @@ def standardize_weights( return _standardize_mmad(weights, scaling_factor=mmad_scaling_factor) else: - raise ValueError(f"Unknown standardization method: {method}") + raise ValueError( + f"Invalid standardization method '{method}'. " + f"Supported: {', '.join(STANDARDIZATION_TYPES)}" + ) def _normalize_sigmoid( @@ -625,7 +631,10 @@ def normalize_weights( elif normalization == NORMALIZATION_TYPES[5]: # "rank" normalized_weights = _normalize_rank(standardized_weights, method=rank_method) else: - raise ValueError(f"Unknown normalization method: {normalization}") + raise ValueError( + f"Invalid normalization method '{normalization}'. " + f"Supported: {', '.join(NORMALIZATION_TYPES)}" + ) # Phase 3: Post-processing if not np.isclose(gamma, 1.0) and np.isfinite(gamma) and gamma > 0: @@ -778,7 +787,10 @@ def calculate_hybrid_extrema_weights( weights=source_weights_array[:, np.newaxis], ) else: - raise ValueError(f"Unknown hybrid aggregation method: {aggregation}") + raise ValueError( + f"Invalid hybrid aggregation method '{aggregation}'. " + f"Supported: {', '.join(HYBRID_AGGREGATIONS)}" + ) if aggregation_normalization != NORMALIZATION_TYPES[6]: # "none" combined_source_weights_array = normalize_weights( @@ -956,7 +968,10 @@ def compute_extrema_weights( default_weight=np.nanmedian(normalized_weights), ) - raise ValueError(f"Unknown extrema weighting strategy: {strategy}") + raise ValueError( + f"Invalid extrema weighting strategy '{strategy}'. " + f"Supported: {', '.join(WEIGHT_STRATEGIES)}" + ) def _apply_weights( @@ -1044,7 +1059,7 @@ def get_weighted_extrema( def get_callable_sha256(fn: Callable[..., Any]) -> str: if not callable(fn): - raise ValueError("fn must be callable") + raise ValueError("Invalid fn: must be callable") code = getattr(fn, "__code__", None) if code is None and isinstance(fn, functools.partial): fn = fn.func @@ -1056,7 +1071,7 @@ def get_callable_sha256(fn: Callable[..., Any]) -> str: if code is None and hasattr(fn, "__call__"): code = getattr(fn.__call__, "__code__", None) if code is None: - raise ValueError("Unable to retrieve code object from fn") + raise ValueError("Invalid fn: unable to retrieve code object") return hashlib.sha256(code.co_code).hexdigest() @@ -1115,7 +1130,7 @@ def top_change_percent(dataframe: pd.DataFrame, period: int) -> pd.Series: :return: The top change percentage series """ if period < 1: - raise ValueError("period must be greater than or equal to 1") + raise ValueError(f"Invalid period {period}: must be >= 1") previous_close_top = ( dataframe.get("close").rolling(period, min_periods=period).max().shift(1) @@ -1133,7 +1148,7 @@ def bottom_change_percent(dataframe: pd.DataFrame, period: int) -> pd.Series: :return: The bottom change percentage series """ if period < 1: - raise ValueError("period must be greater than or equal to 1") + raise ValueError(f"Invalid period {period}: must be >= 1") previous_close_bottom = ( dataframe.get("close").rolling(period, min_periods=period).min().shift(1) @@ -1152,7 +1167,7 @@ def price_retracement_percent(dataframe: pd.DataFrame, period: int) -> pd.Series :return: Retracement percentage series """ if period < 1: - raise ValueError("period must be greater than or equal to 1") + raise ValueError(f"Invalid period {period}: must be >= 1") previous_close_low = ( dataframe.get("close").rolling(period, min_periods=period).min().shift(1) @@ -1234,7 +1249,7 @@ def _fractal_dimension( ) -> float: """Original fractal dimension computation implementation per Ehlers' paper.""" if period % 2 != 0: - raise ValueError("period must be even") + raise ValueError(f"Invalid period {period}: must be even") half_period = period // 2 @@ -1263,7 +1278,7 @@ def frama(df: pd.DataFrame, period: int = 16, zero_lag: bool = False) -> pd.Seri Original FRAMA implementation per Ehlers' paper with optional zero lag. """ if period % 2 != 0: - raise ValueError("period must be even") + raise ValueError(f"Invalid period {period}: must be even") n = len(df) @@ -1305,7 +1320,7 @@ def smma(series: pd.Series, period: int, zero_lag=False, offset=0) -> pd.Series: https://www.sierrachart.com/index.php?page=doc/StudiesReference.php&ID=173&Name=Moving_Average_-_Smoothed """ if period <= 0: - raise ValueError("period must be greater than 0") + raise ValueError(f"Invalid period {period}: must be > 0") n = len(series) if n < period: return pd.Series(index=series.index, dtype=float) @@ -1945,7 +1960,7 @@ def get_optuna_callbacks( ] else: raise ValueError( - f"Unsupported regressor model: {regressor} (supported: {', '.join(REGRESSORS)})" + f"Invalid regressor '{regressor}'. Supported: {', '.join(REGRESSORS)}" ) return callbacks @@ -2016,7 +2031,7 @@ def fit_regressor( ) else: raise ValueError( - f"Unsupported regressor model: {regressor} (supported: {', '.join(REGRESSORS)})" + f"Invalid regressor '{regressor}'. Supported: {', '.join(REGRESSORS)}" ) return model @@ -2030,13 +2045,13 @@ def get_optuna_study_model_parameters( ) -> dict[str, Any]: if regressor not in set(REGRESSORS): raise ValueError( - f"Unsupported regressor model: {regressor} (supported: {', '.join(REGRESSORS)})" + f"Invalid regressor '{regressor}'. Supported: {', '.join(REGRESSORS)}" ) if not isinstance(expansion_ratio, (int, float)) or not ( 0.0 <= expansion_ratio <= 1.0 ): raise ValueError( - f"expansion_ratio must be a float between 0 and 1, got {expansion_ratio}" + f"Invalid expansion_ratio {expansion_ratio}: must be a float between 0 and 1" ) default_ranges: dict[str, tuple[float, float]] = { "n_estimators": (100, 2000), @@ -2162,9 +2177,9 @@ def get_optuna_study_model_parameters( @lru_cache(maxsize=128) def largest_divisor_to_step(integer: int, step: int) -> Optional[int]: if not isinstance(integer, int) or integer <= 0: - raise ValueError("integer must be a positive integer") + raise ValueError(f"Invalid integer {integer!r}: must be a positive integer") if not isinstance(step, int) or step <= 0: - raise ValueError("step must be a positive integer") + raise ValueError(f"Invalid step {step!r}: must be a positive integer") if step == 1 or integer % step == 0: return integer @@ -2216,7 +2231,8 @@ def get_min_max_label_period_candles( ) -> tuple[int, int, int]: if min_label_period_candles > max_label_period_candles: raise ValueError( - "min_label_period_candles must be less than or equal to max_label_period_candles" + f"Invalid label_period_candles range: min ({min_label_period_candles}) " + f"must be <= max ({max_label_period_candles})" ) capped_period_candles = max(1, floor_to_step(max_period_candles, candles_step)) @@ -2268,9 +2284,9 @@ def round_to_step(value: float | int, step: int) -> int: :raises ValueError: If step is not a positive integer or value is not finite. """ if not isinstance(value, (int, float)): - raise ValueError("value must be an integer or float") + raise ValueError(f"Invalid value {value!r}: must be an integer or float") if not isinstance(step, int) or step <= 0: - raise ValueError("step must be a positive integer") + raise ValueError(f"Invalid step {step!r}: must be a positive integer") if isinstance(value, (int, np.integer)): q, r = divmod(value, step) twice_r = r * 2 @@ -2280,33 +2296,33 @@ def round_to_step(value: float | int, step: int) -> int: return (q + 1) * step return int(round(value / step) * step) if not np.isfinite(value): - raise ValueError("value must be finite") + raise ValueError(f"Invalid value {value!r}: must be finite") return int(round(float(value) / step) * step) @lru_cache(maxsize=128) def ceil_to_step(value: float | int, step: int) -> int: if not isinstance(value, (int, float)): - raise ValueError("value must be an integer or float") + raise ValueError(f"Invalid value {value!r}: must be an integer or float") if not isinstance(step, int) or step <= 0: - raise ValueError("step must be a positive integer") + raise ValueError(f"Invalid step {step!r}: must be a positive integer") if isinstance(value, (int, np.integer)): return int(-(-int(value) // step) * step) if not np.isfinite(value): - raise ValueError("value must be finite") + raise ValueError(f"Invalid value {value!r}: must be finite") return int(math.ceil(float(value) / step) * step) @lru_cache(maxsize=128) def floor_to_step(value: float | int, step: int) -> int: if not isinstance(value, (int, float)): - raise ValueError("value must be an integer or float") + raise ValueError(f"Invalid value {value!r}: must be an integer or float") if not isinstance(step, int) or step <= 0: - raise ValueError("step must be a positive integer") + raise ValueError(f"Invalid step {step!r}: must be a positive integer") if isinstance(value, (int, np.integer)): return int((int(value) // step) * step) if not np.isfinite(value): - raise ValueError("value must be finite") + raise ValueError(f"Invalid value {value!r}: must be finite") return int(math.floor(float(value) / step) * step)