Coverage for src/jquantstats/_plots/_portfolio/_diagnostics.py: 100%

15 statements  

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

1"""Diagnostic charts: lead/lag IR, correlation, monthly calendar, cost impact. 

2 

3Split out of the former single-module `_plots/_portfolio.py`; composed into 

4:class:`PortfolioPlots` by `_core.py`. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import TYPE_CHECKING, Literal, overload 

10 

11from .._render import render 

12from .._specs import ( 

13 correlation_heatmap_spec, 

14 lead_lag_ir_spec, 

15 monthly_returns_heatmap_spec, 

16 trading_cost_impact_spec, 

17) 

18 

19if TYPE_CHECKING: 

20 import polars as pl 

21 from matplotlib.figure import Figure as MplFigure 

22 from plotly.graph_objects import Figure as PlotlyFigure 

23 

24 from .._backend import Backend 

25 from .._protocol import PortfolioLike 

26 from .._render import Figure 

27 

28 

29class _DiagnosticPlotsMixin: 

30 """Diagnostic charts for :class:`PortfolioPlots`.""" 

31 

32 __slots__ = () 

33 

34 _portfolio: PortfolioLike 

35 

36 @overload 

37 def lead_lag_ir_plot( 

38 self, start: int = ..., end: int = ..., *, backend: Literal["plotly"] | None = ... 

39 ) -> PlotlyFigure: ... 

40 

41 @overload 

42 def lead_lag_ir_plot(self, start: int = ..., end: int = ..., *, backend: Literal["matplotlib"]) -> MplFigure: ... 

43 

44 def lead_lag_ir_plot( 

45 self, 

46 start: int = -10, 

47 end: int = 19, 

48 *, 

49 backend: Backend | None = None, 

50 ) -> Figure: 

51 """Plot Sharpe ratio (IR) across lead/lag variants of the portfolio. 

52 

53 Builds portfolios with cash positions lagged from ``start`` to ``end`` 

54 (inclusive) and plots a bar chart of the Sharpe ratio for each lag. 

55 Positive lags delay weights; negative lags lead them. 

56 

57 Args: 

58 start: First lag to include (default: -10). 

59 end: Last lag to include (default: +19). 

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

61 

62 Returns: 

63 Figure: One bar per lag, labelled by the lag value. 

64 

65 Raises: 

66 TypeError: If ``start`` or ``end`` is not an integer. 

67 

68 """ 

69 return render(lead_lag_ir_spec(self._portfolio, start, end), backend) 

70 

71 @overload 

72 def correlation_heatmap( 

73 self, 

74 frame: pl.DataFrame | None = ..., 

75 name: str = ..., 

76 title: str = ..., 

77 *, 

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

79 ) -> PlotlyFigure: ... 

80 

81 @overload 

82 def correlation_heatmap( 

83 self, 

84 frame: pl.DataFrame | None = ..., 

85 name: str = ..., 

86 title: str = ..., 

87 *, 

88 backend: Literal["matplotlib"], 

89 ) -> MplFigure: ... 

90 

91 def correlation_heatmap( 

92 self, 

93 frame: pl.DataFrame | None = None, 

94 name: str = "portfolio", 

95 title: str = "Correlation heatmap", 

96 *, 

97 backend: Backend | None = None, 

98 ) -> Figure: 

99 """Plot the correlation matrix of the holdings and the portfolio. 

100 

101 Args: 

102 frame: Series to correlate against. Defaults to the portfolio's 

103 prices. 

104 name: Column name given to the portfolio's own profit series. 

105 title: Chart title. 

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

107 

108 Returns: 

109 Figure: A square correlation matrix. 

110 

111 """ 

112 return render(correlation_heatmap_spec(self._portfolio, frame, name, title), backend) 

113 

114 @overload 

115 def monthly_returns_heatmap(self, *, backend: Literal["plotly"] | None = ...) -> PlotlyFigure: ... 

116 

117 @overload 

118 def monthly_returns_heatmap(self, *, backend: Literal["matplotlib"]) -> MplFigure: ... 

119 

120 def monthly_returns_heatmap(self, *, backend: Backend | None = None) -> Figure: 

121 """Plot a monthly returns calendar heatmap. 

122 

123 Groups portfolio returns by calendar year and month, then renders a 

124 heatmap with months on the x-axis and years on the y-axis. Green 

125 cells indicate positive months; red cells indicate negative months. 

126 Cell text shows the percentage return for that month. 

127 

128 Args: 

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

130 

131 Returns: 

132 Figure: A calendar heatmap of monthly returns. 

133 

134 """ 

135 return render(monthly_returns_heatmap_spec(self._portfolio), backend) 

136 

137 @overload 

138 def trading_cost_impact_plot( 

139 self, max_bps: int = ..., *, backend: Literal["plotly"] | None = ... 

140 ) -> PlotlyFigure: ... 

141 

142 @overload 

143 def trading_cost_impact_plot(self, max_bps: int = ..., *, backend: Literal["matplotlib"]) -> MplFigure: ... 

144 

145 def trading_cost_impact_plot(self, max_bps: int = 20, *, backend: Backend | None = None) -> Figure: 

146 """Plot the Sharpe ratio as a function of one-way trading costs. 

147 

148 Evaluates the portfolio's annualised Sharpe ratio at each integer 

149 cost level from 0 up to ``max_bps`` basis points and renders the 

150 result as a line chart. The zero-cost Sharpe is shown as a 

151 reference horizontal line so that the reader can quickly gauge 

152 at what cost level the strategy's edge is eroded. 

153 

154 Args: 

155 max_bps: Maximum one-way trading cost to evaluate, in basis 

156 points. Defaults to 20. 

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

158 

159 Returns: 

160 Figure: One line showing Sharpe against cost. 

161 

162 Raises: 

163 ValueError: If ``max_bps`` is not a positive integer. 

164 

165 """ 

166 return render(trading_cost_impact_spec(self._portfolio, max_bps), backend)