Coverage for src/basanos/exceptions.py: 100%
48 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-23 05:58 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-23 05:58 +0000
1"""Domain-specific exception types for the Basanos package.
3This module defines a hierarchy of exceptions that provide meaningful context
4when data-validation errors occur within the library.
6All exceptions inherit from `BasanosError` so callers can catch the
7entire family with a single ``except BasanosError`` clause if they prefer.
9Linear-algebra exceptions (``DimensionMismatchError``, ``NonSquareMatrixError``,
10``SingularMatrixError``) extend their ``cvx.linalg`` counterparts and are
11importable directly from this module.
12"""
14from __future__ import annotations
16from cvx.linalg import DimensionMismatchError as _DimensionMismatchError
17from cvx.linalg import NonSquareMatrixError as _NonSquareMatrixError
18from cvx.linalg import SingularMatrixError as _SingularMatrixError
21class BasanosError(Exception):
22 """Base class for all Basanos domain errors."""
25class DimensionMismatchError(_DimensionMismatchError):
26 """Raised when matrix or vector dimensions are incompatible."""
29class NonSquareMatrixError(_NonSquareMatrixError):
30 """Raised when a square matrix is required but not provided."""
33class SingularMatrixError(_SingularMatrixError):
34 """Raised when a matrix is singular and cannot be solved."""
37class InsufficientDataError(BasanosError, ValueError):
38 """Raised when there are too few finite entries to perform a computation.
40 Examples:
41 >>> raise InsufficientDataError("All diagonal entries are non-finite.")
42 Traceback (most recent call last):
43 ...
44 basanos.exceptions.InsufficientDataError: All diagonal entries are non-finite.
45 """
47 def __init__(self, detail: str = "") -> None:
48 """Initialize with an optional detail message that overrides the default."""
49 msg = "Insufficient finite data to complete the computation."
50 if detail:
51 msg = detail
52 super().__init__(msg)
55class MissingDateColumnError(BasanosError, ValueError):
56 """Raised when a required ``'date'`` column is absent from a DataFrame.
58 Args:
59 frame_name: Descriptive name of the frame missing the column (e.g. ``"prices"``).
61 Examples:
62 >>> raise MissingDateColumnError("prices")
63 Traceback (most recent call last):
64 ...
65 basanos.exceptions.MissingDateColumnError: DataFrame 'prices' is missing the required 'date' column.
66 """
68 def __init__(self, frame_name: str) -> None:
69 """Initialize with the name of the frame that is missing the column."""
70 super().__init__(f"DataFrame '{frame_name}' is missing the required 'date' column.")
71 self.frame_name = frame_name
74class ShapeMismatchError(BasanosError, ValueError):
75 """Raised when two DataFrames have incompatible shapes.
77 Args:
78 prices_shape: Shape of the prices DataFrame.
79 mu_shape: Shape of the mu DataFrame.
81 Examples:
82 >>> raise ShapeMismatchError((10, 3), (9, 3))
83 Traceback (most recent call last):
84 ...
85 basanos.exceptions.ShapeMismatchError: 'prices' and 'mu' must have the same shape, got (10, 3) vs (9, 3).
86 """
88 def __init__(self, prices_shape: tuple[int, int], mu_shape: tuple[int, int]) -> None:
89 """Initialize with the shapes of the two mismatched DataFrames."""
90 super().__init__(f"'prices' and 'mu' must have the same shape, got {prices_shape} vs {mu_shape}.")
91 self.prices_shape = prices_shape
92 self.mu_shape = mu_shape
95class ColumnMismatchError(BasanosError, ValueError):
96 """Raised when two DataFrames have different column sets.
98 Args:
99 prices_columns: Columns of the prices DataFrame.
100 mu_columns: Columns of the mu DataFrame.
102 Examples:
103 >>> raise ColumnMismatchError(["A", "B"], ["A", "C"]) # doctest: +ELLIPSIS
104 Traceback (most recent call last):
105 ...
106 basanos.exceptions.ColumnMismatchError: 'prices' and 'mu' must have identical columns...
107 """
109 def __init__(self, prices_columns: list[str], mu_columns: list[str]) -> None:
110 """Initialize with the column lists of the two mismatched DataFrames."""
111 super().__init__(
112 f"'prices' and 'mu' must have identical columns; got {sorted(prices_columns)} vs {sorted(mu_columns)}."
113 )
114 self.prices_columns = prices_columns
115 self.mu_columns = mu_columns
118class NonPositivePricesError(BasanosError, ValueError):
119 """Raised when an asset column contains zero or negative prices.
121 Log-return computation requires strictly positive prices.
123 Args:
124 asset: Name of the asset with the offending values.
126 Examples:
127 >>> raise NonPositivePricesError("A") # doctest: +ELLIPSIS
128 Traceback (most recent call last):
129 ...
130 basanos.exceptions.NonPositivePricesError: Asset 'A' contains non-positive...
131 """
133 def __init__(self, asset: str) -> None:
134 """Initialize with the name of the asset that contains non-positive prices."""
135 super().__init__(f"Asset '{asset}' contains non-positive prices; strictly positive values are required.")
136 self.asset = asset
139class ExcessiveNullsError(BasanosError, ValueError):
140 """Raised when an asset column contains too many null values.
142 Args:
143 asset: Name of the offending asset column.
144 null_fraction: Observed fraction of null values (0.0 to 1.0).
145 max_fraction: Maximum allowed fraction of null values.
147 Examples:
148 >>> raise ExcessiveNullsError("A", 1.0, 0.9) # doctest: +ELLIPSIS
149 Traceback (most recent call last):
150 ...
151 basanos.exceptions.ExcessiveNullsError: Asset 'A' has 100% null values,...
152 """
154 def __init__(self, asset: str, null_fraction: float, max_fraction: float) -> None:
155 """Initialize with the asset name and the observed/maximum null fractions."""
156 super().__init__(
157 f"Asset '{asset}' has {null_fraction:.0%} null values, "
158 f"exceeding the maximum allowed fraction of {max_fraction:.0%}."
159 )
160 self.asset = asset
161 self.null_fraction = null_fraction
162 self.max_fraction = max_fraction
165class MonotonicPricesError(BasanosError, ValueError):
166 """Raised when an asset's price series is strictly monotonic.
168 A monotonic series (all non-decreasing or all non-increasing) has no
169 variance in its return sign, indicating malformed or synthetic data.
171 Args:
172 asset: Name of the offending asset column.
174 Examples:
175 >>> raise MonotonicPricesError("A") # doctest: +ELLIPSIS
176 Traceback (most recent call last):
177 ...
178 basanos.exceptions.MonotonicPricesError: Asset 'A' has monotonic prices...
179 """
181 def __init__(self, asset: str) -> None:
182 """Initialize with the name of the asset that has monotonic prices."""
183 super().__init__(
184 f"Asset '{asset}' has monotonic prices "
185 "(all non-decreasing or all non-increasing), indicating malformed or synthetic data."
186 )
187 self.asset = asset
190class FactorModelError(BasanosError, ValueError):
191 """Raised when `FactorModel` arguments fail validation.
193 Covers shape mismatches between factor loadings, factor covariance, and
194 idiosyncratic variance arrays, non-positive idiosyncratic variances,
195 invalid return matrix dimensionality, and out-of-range factor counts.
197 Examples:
198 >>> raise FactorModelError("factor_loadings must be 2-D, got ndim=1.")
199 Traceback (most recent call last):
200 ...
201 basanos.exceptions.FactorModelError: factor_loadings must be 2-D, got ndim=1.
202 """
205class StreamStateCorruptError(BasanosError, ValueError):
206 """Raised when a saved stream archive is missing required state keys.
208 This is raised by `load` when the ``.npz`` archive
209 does not contain one or more keys that the current `_StreamState`
210 schema requires. The error message lists the missing keys so callers
211 can diagnose schema mismatches immediately rather than seeing a bare
212 ``KeyError`` with no context.
214 Args:
215 missing: Collection of key names absent from the archive.
217 Examples:
218 >>> raise StreamStateCorruptError({"vola_s_x", "corr_ret_buf"}) # doctest: +ELLIPSIS
219 Traceback (most recent call last):
220 ...
221 basanos.exceptions.StreamStateCorruptError: Stream archive is missing required keys: ...
222 """
224 def __init__(self, missing: set[str] | frozenset[str]) -> None:
225 """Initialize with the set of missing archive keys."""
226 keys = ", ".join(sorted(missing))
227 super().__init__(
228 f"Stream archive is missing required keys: {keys}. "
229 "Re-generate the archive via BasanosStream.from_warmup() and save()."
230 )
231 self.missing = frozenset(missing)