扩展指标
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Custom exception hierarchy for ferro_ta.
|
||||
|
||||
Exception classes
|
||||
-----------------
|
||||
FerroTAError — Base class for all ferro_ta exceptions.
|
||||
FerroTAValueError — Raised for invalid parameter values (e.g. timeperiod < 1).
|
||||
FerroTAInputError — Raised for invalid input arrays (e.g. mismatched lengths, wrong dtype, unexpected NaN/Inf when strict mode is used).
|
||||
|
||||
All custom exceptions inherit from both the ferro_ta base and the corresponding
|
||||
built-in exception (ValueError) so that existing ``except ValueError`` clauses
|
||||
continue to work after upgrading.
|
||||
|
||||
Error codes
|
||||
-----------
|
||||
Every exception carries a ``code`` attribute (e.g. ``"FTERR001"``) for
|
||||
programmatic handling:
|
||||
|
||||
FTERR001 — Invalid parameter value (FerroTAValueError)
|
||||
FTERR002 — Invalid input array (FerroTAInputError)
|
||||
FTERR003 — Input array too short (FerroTAInputError)
|
||||
FTERR004 — Input arrays have mismatched lengths (FerroTAInputError)
|
||||
FTERR005 — Input array contains NaN or Inf (FerroTAInputError, strict mode)
|
||||
FTERR006 — General Rust-bridge error (FerroTAValueError or FerroTAInputError)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from ferro_ta.core.exceptions import FerroTAError, FerroTAValueError, FerroTAInputError
|
||||
>>> raise FerroTAValueError("timeperiod must be >= 1, got 0")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ferro_ta.exceptions.FerroTAValueError: [FTERR001] timeperiod must be >= 1, got 0
|
||||
>>> try:
|
||||
... raise FerroTAValueError("bad value")
|
||||
... except FerroTAValueError as exc:
|
||||
... print(exc.code)
|
||||
FTERR001
|
||||
|
||||
NaN / Inf policy
|
||||
----------------
|
||||
By default ferro_ta **propagates** NaN and Inf in input arrays — output values
|
||||
that depend on a NaN/Inf input will themselves be NaN/Inf. No exception is
|
||||
raised for NaN or Inf values in the input data. If you need strict mode, call
|
||||
:func:`ferro_ta.exceptions.check_finite` on your arrays before passing them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NoReturn
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error code registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: Maps each ``FerroTAError`` subclass to its default error code.
|
||||
ERROR_CODES: dict[str, str] = {
|
||||
"FerroTAError": "FTERR000",
|
||||
"FerroTAValueError": "FTERR001",
|
||||
"FerroTAInputError": "FTERR002",
|
||||
}
|
||||
|
||||
# Well-known codes for specific error kinds
|
||||
_CODE_TOO_SHORT = "FTERR003"
|
||||
_CODE_LENGTH_MISMATCH = "FTERR004"
|
||||
_CODE_NOT_FINITE = "FTERR005"
|
||||
_CODE_RUST_BRIDGE = "FTERR006"
|
||||
|
||||
# Code descriptions (for reference and programmatic inspection)
|
||||
ERROR_CODE_DESCRIPTIONS: dict[str, str] = {
|
||||
"FTERR000": "General ferro_ta error (base class fallback)",
|
||||
"FTERR001": "Invalid parameter value",
|
||||
"FTERR002": "Invalid input array",
|
||||
"FTERR003": "Input array too short",
|
||||
"FTERR004": "Input arrays have mismatched lengths",
|
||||
"FTERR005": "Input array contains NaN or Inf (strict mode)",
|
||||
"FTERR006": "Rust-bridge error (re-raised from Rust ValueError)",
|
||||
}
|
||||
|
||||
|
||||
class FerroTAError(Exception):
|
||||
"""Base class for all ferro_ta exceptions.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
code : str
|
||||
A short error code string (e.g. ``"FTERR001"``) for programmatic
|
||||
handling. The code is included at the beginning of the exception
|
||||
message.
|
||||
suggestion : str | None
|
||||
Optional human-readable suggestion for how to fix the error.
|
||||
"""
|
||||
|
||||
code: str = "FTERR000"
|
||||
suggestion: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
code: str | None = None,
|
||||
suggestion: str | None = None,
|
||||
) -> None:
|
||||
self.code = code or type(self).code
|
||||
self.suggestion = suggestion
|
||||
full_msg = f"[{self.code}] {message}"
|
||||
if suggestion:
|
||||
full_msg = f"{full_msg}\n Suggestion: {suggestion}"
|
||||
super().__init__(full_msg)
|
||||
|
||||
|
||||
class FerroTAValueError(FerroTAError, ValueError):
|
||||
"""Raised when a parameter value is out of the accepted range.
|
||||
|
||||
Examples: ``timeperiod < 1``, ``fastperiod >= slowperiod`` for MACD.
|
||||
|
||||
Default error code: ``FTERR001``.
|
||||
"""
|
||||
|
||||
code = "FTERR001"
|
||||
|
||||
|
||||
class FerroTAInputError(FerroTAError, ValueError):
|
||||
"""Raised when one or more input arrays are invalid.
|
||||
|
||||
Examples: mismatched lengths for open/high/low/close, wrong dtype that
|
||||
cannot be coerced to float64.
|
||||
|
||||
Default error code: ``FTERR002``.
|
||||
"""
|
||||
|
||||
code = "FTERR002"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Finer-grained exception subclasses (added in 1.2.0).
|
||||
#
|
||||
# These are drop-in compatible with the base classes: every subclass still
|
||||
# inherits from ``FerroTAError`` and ``ValueError``, so existing user code
|
||||
# like ``except FerroTAValueError:`` or ``except ValueError:`` keeps working.
|
||||
# The subclasses exist so users can catch *specific* failure modes without
|
||||
# string-matching on the error message.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class InvalidPeriodError(FerroTAValueError):
|
||||
"""Parameter like ``timeperiod``, ``fastperiod``, ``slowperiod`` is out of range.
|
||||
|
||||
Default error code: ``FTERR001``.
|
||||
"""
|
||||
|
||||
|
||||
class InsufficientDataError(FerroTAInputError):
|
||||
"""Input array is shorter than the minimum required for the indicator.
|
||||
|
||||
Default error code: ``FTERR003``.
|
||||
"""
|
||||
|
||||
code = "FTERR003"
|
||||
|
||||
|
||||
class LengthMismatchError(FerroTAInputError):
|
||||
"""Two or more input arrays (e.g. OHLC) have different lengths.
|
||||
|
||||
Default error code: ``FTERR004``.
|
||||
"""
|
||||
|
||||
code = "FTERR004"
|
||||
|
||||
|
||||
class NumericConvergenceError(FerroTAValueError):
|
||||
"""An iterative calculation failed to converge within tolerance.
|
||||
|
||||
Raised by iterative pricing models (implied volatility root-finding,
|
||||
Newton-Raphson, etc.) when the maximum iteration count is exhausted.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidInputError(FerroTAInputError):
|
||||
"""Input contains NaN/Inf in strict mode, wrong dtype, or wrong shape.
|
||||
|
||||
Default error code: ``FTERR005``.
|
||||
"""
|
||||
|
||||
code = "FTERR005"
|
||||
|
||||
|
||||
# Public aliases that match the names documented in the README and
|
||||
# CHANGELOG [Unreleased] section.
|
||||
FerroTaError = FerroTAError # type: ignore[misc]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Validation helpers (called by Python wrappers)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def check_timeperiod(value: int, name: str = "timeperiod", minimum: int = 1) -> None:
|
||||
"""Raise :class:`FerroTAValueError` if *value* < *minimum*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value:
|
||||
The period parameter to validate.
|
||||
name:
|
||||
Human-readable parameter name for the error message.
|
||||
minimum:
|
||||
Minimum acceptable value (default 1).
|
||||
|
||||
Raises
|
||||
------
|
||||
FerroTAValueError
|
||||
If ``value < minimum``.
|
||||
"""
|
||||
if value < minimum:
|
||||
raise InvalidPeriodError(
|
||||
f"{name} must be >= {minimum}, got {value}",
|
||||
suggestion=f"Set {name}={minimum} or higher.",
|
||||
)
|
||||
|
||||
|
||||
def check_equal_length(**arrays: object) -> None:
|
||||
"""Raise :class:`FerroTAInputError` if the supplied arrays differ in length.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**arrays:
|
||||
Keyword arguments mapping name → array-like. At least two arrays
|
||||
should be supplied for the check to be meaningful.
|
||||
|
||||
Raises
|
||||
------
|
||||
FerroTAInputError
|
||||
If any two arrays have different lengths.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from ferro_ta.core.exceptions import check_equal_length
|
||||
>>> check_equal_length(open=np.array([1.0]), close=np.array([1.0, 2.0]))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ferro_ta.exceptions.FerroTAInputError: ...
|
||||
"""
|
||||
|
||||
lengths = {}
|
||||
for name, arr in arrays.items():
|
||||
if hasattr(arr, "__len__"):
|
||||
lengths[name] = len(arr) # type: ignore[arg-type]
|
||||
elif hasattr(arr, "shape"):
|
||||
lengths[name] = arr.shape[0] # type: ignore[union-attr]
|
||||
|
||||
if len(set(lengths.values())) > 1:
|
||||
detail = ", ".join(f"{k}={v}" for k, v in lengths.items())
|
||||
raise LengthMismatchError(
|
||||
f"All input arrays must have the same length. Got: {detail}",
|
||||
code=_CODE_LENGTH_MISMATCH,
|
||||
suggestion="Trim or align your arrays so that open, high, low, close, and volume all have the same number of rows.",
|
||||
)
|
||||
|
||||
|
||||
def check_finite(arr: object, name: str = "input") -> None:
|
||||
"""Raise :class:`FerroTAInputError` if *arr* contains NaN or Inf.
|
||||
|
||||
This is an *opt-in* strict-mode helper. ferro_ta does **not** call this
|
||||
automatically — it is provided for users who want deterministic behaviour
|
||||
when their data may contain missing values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arr:
|
||||
Array-like to check.
|
||||
name:
|
||||
Human-readable name used in the error message.
|
||||
|
||||
Raises
|
||||
------
|
||||
FerroTAInputError
|
||||
If any element of *arr* is NaN or Inf.
|
||||
"""
|
||||
import numpy as np # local import
|
||||
|
||||
a = np.asarray(arr, dtype=np.float64)
|
||||
if not np.all(np.isfinite(a)):
|
||||
raise InvalidInputError(
|
||||
f"{name} contains NaN or Inf values. "
|
||||
"ferro_ta propagates NaN by default; call check_finite() only "
|
||||
"when you require all-finite inputs.",
|
||||
code=_CODE_NOT_FINITE,
|
||||
suggestion="Use numpy.nan_to_num() or dropna() to clean your data before passing it to ferro_ta.",
|
||||
)
|
||||
|
||||
|
||||
def check_min_length(arr: object, min_len: int, name: str = "input") -> None:
|
||||
"""Raise :class:`FerroTAInputError` if *arr* has length less than *min_len*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arr:
|
||||
Array-like to check.
|
||||
min_len:
|
||||
Minimum required length.
|
||||
name:
|
||||
Human-readable name used in the error message.
|
||||
|
||||
Raises
|
||||
------
|
||||
FerroTAInputError
|
||||
If ``len(arr) < min_len``.
|
||||
"""
|
||||
length = 0
|
||||
if hasattr(arr, "__len__"):
|
||||
length = len(arr) # type: ignore[arg-type]
|
||||
elif hasattr(arr, "shape"):
|
||||
length = arr.shape[0] # type: ignore[union-attr]
|
||||
if length < min_len:
|
||||
raise InsufficientDataError(
|
||||
f"{name} must have at least {min_len} elements, got {length}",
|
||||
code=_CODE_TOO_SHORT,
|
||||
suggestion=f"Provide at least {min_len} data points. Current length: {length}.",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_rust_error(err: ValueError) -> NoReturn:
|
||||
"""Re-raise a Rust-originated ValueError as FerroTAValueError or FerroTAInputError.
|
||||
|
||||
Used by Python wrappers so users can catch FerroTA* exceptions consistently.
|
||||
"""
|
||||
msg = str(err).lower()
|
||||
if (
|
||||
"length" in msg
|
||||
or "same length" in msg
|
||||
or "array" in msg
|
||||
or "mismatch" in msg
|
||||
or "dimension" in msg
|
||||
or "1-d" in msg
|
||||
):
|
||||
raise FerroTAInputError(str(err), code=_CODE_RUST_BRIDGE) from err
|
||||
raise FerroTAValueError(str(err), code=_CODE_RUST_BRIDGE) from err
|
||||
Reference in New Issue
Block a user