Coverage for src/jquantstats/_plots/_data/_cumulative.py: 100%

16 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 04:11 +0000

1"""Cumulative-return and equity-curve line charts.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Literal, overload 

6 

7from .._render import render 

8from .._specs import compare_spec, cumulative_returns_spec, earnings_spec, log_returns_spec 

9 

10if TYPE_CHECKING: 

11 from matplotlib.figure import Figure as MplFigure 

12 from plotly.graph_objects import Figure as PlotlyFigure 

13 

14 from jquantstats._protocol import DataLike 

15 

16 from .._backend import Backend 

17 from .._render import Figure 

18 

19 

20class _CumulativePlotsMixin: 

21 """Cumulative-return and equity-curve plots for :class:`DataPlots`. 

22 

23 Every method here accepts a ``backend`` keyword selecting the renderer, and 

24 is overloaded on it: omitting it (or naming ``"plotly"``) is typed as 

25 returning a `plotly.graph_objects.Figure`, so callers written before the 

26 matplotlib backend existed keep exactly the type they had. 

27 

28 One caveat that no type system can express: after 

29 `jquantstats.set_plot_backend` changes the process-wide default, a call 

30 that passes no ``backend`` returns the new default's figure type while 

31 still being *typed* as Plotly's. Pass ``backend=`` explicitly if you change 

32 the global default and care about static types. 

33 """ 

34 

35 __slots__ = () 

36 

37 _data: DataLike 

38 

39 @overload 

40 def returns( 

41 self, title: str = ..., log_scale: bool = ..., *, backend: Literal["plotly"] | None = ... 

42 ) -> PlotlyFigure: ... 

43 

44 @overload 

45 def returns(self, title: str = ..., log_scale: bool = ..., *, backend: Literal["matplotlib"]) -> MplFigure: ... 

46 

47 def returns( 

48 self, 

49 title: str = "Cumulative Returns", 

50 log_scale: bool = False, 

51 *, 

52 backend: Backend | None = None, 

53 ) -> Figure: 

54 """Cumulative compounded returns over time. 

55 

56 Plots ``(1 + r).cumprod()`` for every column in the dataset (including 

57 benchmark when present). 

58 

59 Args: 

60 title: Chart title. Defaults to ``"Cumulative Returns"``. 

61 log_scale: Use a logarithmic y-axis. Defaults to False. 

62 backend: Renderer to use. Defaults to the ambient selection. 

63 

64 Returns: 

65 Figure: A line chart. 

66 

67 """ 

68 return render(cumulative_returns_spec(self._data, title=title, log_scale=log_scale), backend) 

69 

70 @overload 

71 def compare( 

72 self, 

73 title: str = ..., 

74 figsize: tuple[int, int] | None = ..., 

75 *, 

76 backend: Literal["plotly"] | None = ..., 

77 ) -> PlotlyFigure: ... 

78 

79 @overload 

80 def compare( 

81 self, title: str = ..., figsize: tuple[int, int] | None = ..., *, backend: Literal["matplotlib"] 

82 ) -> MplFigure: ... 

83 

84 def compare( 

85 self, 

86 title: str = "Comparison vs Benchmark", 

87 figsize: tuple[int, int] | None = None, 

88 *, 

89 backend: Backend | None = None, 

90 ) -> Figure: 

91 """Compare cumulative returns of each asset against the benchmark. 

92 

93 Args: 

94 title: Chart title. Defaults to ``"Comparison vs Benchmark"``. 

95 figsize: Optional ``(width, height)`` in pixels. 

96 backend: Renderer to use. Defaults to the ambient selection. 

97 

98 Returns: 

99 Figure: A line chart. 

100 

101 Raises: 

102 AttributeError: If no benchmark data is available. 

103 

104 """ 

105 return render(compare_spec(self._data, title=title, figsize=figsize), backend) 

106 

107 @overload 

108 def log_returns( 

109 self, 

110 title: str = ..., 

111 figsize: tuple[int, int] | None = ..., 

112 *, 

113 backend: Literal["plotly"] | None = ..., 

114 ) -> PlotlyFigure: ... 

115 

116 @overload 

117 def log_returns( 

118 self, title: str = ..., figsize: tuple[int, int] | None = ..., *, backend: Literal["matplotlib"] 

119 ) -> MplFigure: ... 

120 

121 def log_returns( 

122 self, 

123 title: str = "Log Returns", 

124 figsize: tuple[int, int] | None = None, 

125 *, 

126 backend: Backend | None = None, 

127 ) -> Figure: 

128 """Cumulative log returns over time. 

129 

130 Plots ``log((1 + r).cumprod())`` — the natural log of the compounded 

131 growth factor — which linearises exponential growth and makes 

132 multi-asset comparisons on a common scale. 

133 

134 Args: 

135 title: Chart title. Defaults to ``"Log Returns"``. 

136 figsize: Optional ``(width, height)`` in pixels. 

137 backend: Renderer to use. Defaults to the ambient selection. 

138 

139 Returns: 

140 Figure: A line chart. 

141 

142 """ 

143 return render(log_returns_spec(self._data, title=title, figsize=figsize), backend) 

144 

145 @overload 

146 def earnings( 

147 self, 

148 start_balance: float = ..., 

149 title: str = ..., 

150 compounded: bool = ..., 

151 *, 

152 backend: Literal["plotly"] | None = ..., 

153 ) -> PlotlyFigure: ... 

154 

155 @overload 

156 def earnings( 

157 self, 

158 start_balance: float = ..., 

159 title: str = ..., 

160 compounded: bool = ..., 

161 *, 

162 backend: Literal["matplotlib"], 

163 ) -> MplFigure: ... 

164 

165 def earnings( 

166 self, 

167 start_balance: float = 1e5, 

168 title: str = "Portfolio Earnings", 

169 compounded: bool = True, 

170 *, 

171 backend: Backend | None = None, 

172 ) -> Figure: 

173 """Dollar equity curve showing portfolio value over time. 

174 

175 Scales cumulative returns by *start_balance* so the y-axis reflects 

176 an absolute portfolio value rather than a dimensionless growth factor. 

177 

178 Args: 

179 start_balance: Starting portfolio value in currency units. 

180 Defaults to 100 000. 

181 title: Chart title. Defaults to ``"Portfolio Earnings"``. 

182 compounded: Use compounded returns (``cumprod``). When False uses 

183 cumulative sum. Defaults to True. 

184 backend: Renderer to use. Defaults to the ambient selection. 

185 

186 Returns: 

187 Figure: A line chart. 

188 

189 """ 

190 spec = earnings_spec(self._data, start_balance=start_balance, title=title, compounded=compounded) 

191 return render(spec, backend)