Coverage for src / jquantstats / exceptions.py: 100%
34 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-07 14:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-07 14:28 +0000
1"""Domain-specific exception types for the jquantstats package.
3This module defines a hierarchy of exceptions that provide meaningful context
4when data-validation errors occur within the package.
6All exceptions inherit from :class:`JQuantStatsError` so callers can catch the
7entire family with a single ``except JQuantStatsError`` clause if they prefer.
9Examples:
10 >>> raise MissingDateColumnError("prices") # doctest: +ELLIPSIS
11 Traceback (most recent call last):
12 ...
13 jquantstats.exceptions.MissingDateColumnError: ...
14"""
16from __future__ import annotations
19class JQuantStatsError(Exception):
20 """Base class for all JQuantStats domain errors."""
23class MissingDateColumnError(JQuantStatsError, ValueError):
24 """Raised when a required ``'date'`` column is absent from a DataFrame.
26 Args:
27 frame_name: Descriptive name of the frame missing the column (e.g. ``"prices"``).
29 Examples:
30 >>> raise MissingDateColumnError("prices") # doctest: +ELLIPSIS
31 Traceback (most recent call last):
32 ...
33 jquantstats.exceptions.MissingDateColumnError: ...
34 """
36 def __init__(self, frame_name: str) -> None:
37 """Initialize with the name of the frame that is missing the column."""
38 super().__init__(f"DataFrame '{frame_name}' is missing the required 'date' column.")
39 self.frame_name = frame_name
42class InvalidCashPositionTypeError(JQuantStatsError, TypeError):
43 """Raised when ``cashposition`` is not a :class:`polars.DataFrame`.
45 Args:
46 actual_type: The ``type.__name__`` of the value that was supplied.
48 Examples:
49 >>> raise InvalidCashPositionTypeError("dict")
50 Traceback (most recent call last):
51 ...
52 jquantstats.exceptions.InvalidCashPositionTypeError: cashposition must be pl.DataFrame, got dict.
53 """
55 def __init__(self, actual_type: str) -> None:
56 """Initialize with the offending type name."""
57 super().__init__(f"cashposition must be pl.DataFrame, got {actual_type}.")
58 self.actual_type = actual_type
61class InvalidPricesTypeError(JQuantStatsError, TypeError):
62 """Raised when ``prices`` is not a :class:`polars.DataFrame`.
64 Args:
65 actual_type: The ``type.__name__`` of the value that was supplied.
67 Examples:
68 >>> raise InvalidPricesTypeError("list")
69 Traceback (most recent call last):
70 ...
71 jquantstats.exceptions.InvalidPricesTypeError: prices must be pl.DataFrame, got list.
72 """
74 def __init__(self, actual_type: str) -> None:
75 """Initialize with the offending type name."""
76 super().__init__(f"prices must be pl.DataFrame, got {actual_type}.")
77 self.actual_type = actual_type
80class NonPositiveAumError(JQuantStatsError, ValueError):
81 """Raised when ``aum`` is not strictly positive.
83 Args:
84 aum: The non-positive value that was supplied.
86 Examples:
87 >>> raise NonPositiveAumError(0.0)
88 Traceback (most recent call last):
89 ...
90 jquantstats.exceptions.NonPositiveAumError: aum must be strictly positive, got 0.0.
91 """
93 def __init__(self, aum: float) -> None:
94 """Initialize with the offending aum value."""
95 super().__init__(f"aum must be strictly positive, got {aum}.")
96 self.aum = aum
99class RowCountMismatchError(JQuantStatsError, ValueError):
100 """Raised when ``prices`` and ``cashposition`` have different numbers of rows.
102 Args:
103 prices_rows: Number of rows in the prices DataFrame.
104 cashposition_rows: Number of rows in the cashposition DataFrame.
106 Examples:
107 >>> raise RowCountMismatchError(10, 9) # doctest: +ELLIPSIS
108 Traceback (most recent call last):
109 ...
110 jquantstats.exceptions.RowCountMismatchError: ...
111 """
113 def __init__(self, prices_rows: int, cashposition_rows: int) -> None:
114 """Initialize with the row counts of the two mismatched DataFrames."""
115 super().__init__(
116 f"cashposition and prices must have the same number of rows, "
117 f"got cashposition={cashposition_rows} and prices={prices_rows}."
118 )
119 self.prices_rows = prices_rows
120 self.cashposition_rows = cashposition_rows
123class IntegerIndexBoundError(JQuantStatsError, TypeError):
124 """Raised when a row-index bound is not an integer.
126 Args:
127 param: Name of the offending parameter (e.g. ``"start"`` or ``"end"``).
128 actual_type: The ``type.__name__`` of the value that was supplied.
130 Examples:
131 >>> raise IntegerIndexBoundError("start", "str")
132 Traceback (most recent call last):
133 ...
134 jquantstats.exceptions.IntegerIndexBoundError: start must be an integer, got str.
135 """
137 def __init__(self, param: str, actual_type: str) -> None:
138 """Initialize with the parameter name and the offending type."""
139 super().__init__(f"{param} must be an integer, got {actual_type}.")
140 self.param = param
141 self.actual_type = actual_type
144class NullsInReturnsError(JQuantStatsError, ValueError):
145 """Raised when null values are detected in returns (or benchmark) data.
147 Polars propagates ``null`` through calculations whereas pandas silently
148 drops ``NaN``. Leaving nulls in place will cause most statistics to
149 return ``null`` instead of a numeric result.
151 Use the ``null_strategy`` parameter on :meth:`~jquantstats.data.Data.from_returns`
152 or :meth:`~jquantstats.data.Data.from_prices` to handle nulls automatically, or
153 clean the data before construction.
155 Args:
156 frame_name: Descriptive name of the frame that contains nulls
157 (e.g. ``"returns"`` or ``"benchmark"``).
158 columns: Names of the columns that contain at least one null.
160 Examples:
161 >>> raise NullsInReturnsError("returns", ["Asset1", "Asset2"])
162 Traceback (most recent call last):
163 ...
164 jquantstats.exceptions.NullsInReturnsError: ...
165 """
167 def __init__(self, frame_name: str, columns: list[str]) -> None:
168 """Initialize with the frame name and the columns that contain nulls."""
169 cols_str = ", ".join(f"'{c}'" for c in columns)
170 super().__init__(
171 f"DataFrame '{frame_name}' contains null values in column(s): {cols_str}. "
172 f"Pass null_strategy='drop' or null_strategy='forward_fill' to handle nulls "
173 f"automatically, or clean the data before construction."
174 )
175 self.frame_name = frame_name
176 self.columns = columns