Coverage for src / jquantstats / exceptions.py: 100%

28 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 18:44 +0000

1"""Domain-specific exception types for the jquantstats package. 

2 

3This module defines a hierarchy of exceptions that provide meaningful context 

4when data-validation errors occur within the package. 

5 

6All exceptions inherit from :class:`JQuantStatsError` so callers can catch the 

7entire family with a single ``except JQuantStatsError`` clause if they prefer. 

8 

9Examples: 

10 >>> raise MissingDateColumnError("prices") # doctest: +ELLIPSIS 

11 Traceback (most recent call last): 

12 ... 

13 jquantstats.exceptions.MissingDateColumnError: ... 

14""" 

15 

16from __future__ import annotations 

17 

18 

19class JQuantStatsError(Exception): 

20 """Base class for all JQuantStats domain errors.""" 

21 

22 

23class MissingDateColumnError(JQuantStatsError, ValueError): 

24 """Raised when a required ``'date'`` column is absent from a DataFrame. 

25 

26 Args: 

27 frame_name: Descriptive name of the frame missing the column (e.g. ``"prices"``). 

28 

29 Examples: 

30 >>> raise MissingDateColumnError("prices") # doctest: +ELLIPSIS 

31 Traceback (most recent call last): 

32 ... 

33 jquantstats.exceptions.MissingDateColumnError: ... 

34 """ 

35 

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 

40 

41 

42class InvalidCashPositionTypeError(JQuantStatsError, TypeError): 

43 """Raised when ``cashposition`` is not a :class:`polars.DataFrame`. 

44 

45 Args: 

46 actual_type: The ``type.__name__`` of the value that was supplied. 

47 

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 """ 

54 

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 

59 

60 

61class InvalidPricesTypeError(JQuantStatsError, TypeError): 

62 """Raised when ``prices`` is not a :class:`polars.DataFrame`. 

63 

64 Args: 

65 actual_type: The ``type.__name__`` of the value that was supplied. 

66 

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 """ 

73 

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 

78 

79 

80class NonPositiveAumError(JQuantStatsError, ValueError): 

81 """Raised when ``aum`` is not strictly positive. 

82 

83 Args: 

84 aum: The non-positive value that was supplied. 

85 

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 """ 

92 

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 

97 

98 

99class RowCountMismatchError(JQuantStatsError, ValueError): 

100 """Raised when ``prices`` and ``cashposition`` have different numbers of rows. 

101 

102 Args: 

103 prices_rows: Number of rows in the prices DataFrame. 

104 cashposition_rows: Number of rows in the cashposition DataFrame. 

105 

106 Examples: 

107 >>> raise RowCountMismatchError(10, 9) # doctest: +ELLIPSIS 

108 Traceback (most recent call last): 

109 ... 

110 jquantstats.exceptions.RowCountMismatchError: ... 

111 """ 

112 

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 

121 

122 

123class IntegerIndexBoundError(JQuantStatsError, TypeError): 

124 """Raised when a row-index bound is not an integer. 

125 

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. 

129 

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 """ 

136 

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