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
« 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.
3Split out of the former single-module `_plots/_portfolio.py`; composed into
4:class:`PortfolioPlots` by `_core.py`.
5"""
7from __future__ import annotations
9from typing import TYPE_CHECKING, Literal, overload
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)
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
24 from .._backend import Backend
25 from .._protocol import PortfolioLike
26 from .._render import Figure
29class _DiagnosticPlotsMixin:
30 """Diagnostic charts for :class:`PortfolioPlots`."""
32 __slots__ = ()
34 _portfolio: PortfolioLike
36 @overload
37 def lead_lag_ir_plot(
38 self, start: int = ..., end: int = ..., *, backend: Literal["plotly"] | None = ...
39 ) -> PlotlyFigure: ...
41 @overload
42 def lead_lag_ir_plot(self, start: int = ..., end: int = ..., *, backend: Literal["matplotlib"]) -> MplFigure: ...
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.
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.
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.
62 Returns:
63 Figure: One bar per lag, labelled by the lag value.
65 Raises:
66 TypeError: If ``start`` or ``end`` is not an integer.
68 """
69 return render(lead_lag_ir_spec(self._portfolio, start, end), backend)
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: ...
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: ...
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.
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.
108 Returns:
109 Figure: A square correlation matrix.
111 """
112 return render(correlation_heatmap_spec(self._portfolio, frame, name, title), backend)
114 @overload
115 def monthly_returns_heatmap(self, *, backend: Literal["plotly"] | None = ...) -> PlotlyFigure: ...
117 @overload
118 def monthly_returns_heatmap(self, *, backend: Literal["matplotlib"]) -> MplFigure: ...
120 def monthly_returns_heatmap(self, *, backend: Backend | None = None) -> Figure:
121 """Plot a monthly returns calendar heatmap.
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.
128 Args:
129 backend: Renderer to use. Defaults to the ambient selection.
131 Returns:
132 Figure: A calendar heatmap of monthly returns.
134 """
135 return render(monthly_returns_heatmap_spec(self._portfolio), backend)
137 @overload
138 def trading_cost_impact_plot(
139 self, max_bps: int = ..., *, backend: Literal["plotly"] | None = ...
140 ) -> PlotlyFigure: ...
142 @overload
143 def trading_cost_impact_plot(self, max_bps: int = ..., *, backend: Literal["matplotlib"]) -> MplFigure: ...
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.
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.
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.
159 Returns:
160 Figure: One line showing Sharpe against cost.
162 Raises:
163 ValueError: If ``max_bps`` is not a positive integer.
165 """
166 return render(trading_cost_impact_spec(self._portfolio, max_bps), backend)