Coverage for src/jquantstats/_plots/_data/_drawdown.py: 100%
46 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-06 04:52 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-06 04:52 +0000
1"""Drawdown charts (underwater curve and worst-period shading)."""
3from __future__ import annotations
5from typing import TYPE_CHECKING
7import plotly.express as px
8import plotly.graph_objects as go
9import polars as pl
11from ._styling import _apply_base_layout, _compute_drawdown_periods, _hex_to_rgba, _ticker_colors
13if TYPE_CHECKING:
14 from jquantstats._protocol import DataLike
17class _DrawdownPlotsMixin:
18 """Drawdown plots for :class:`DataPlots`."""
20 __slots__ = ()
22 _data: DataLike
24 def drawdown(self, title: str = "Drawdowns") -> go.Figure:
25 """Underwater equity curve (drawdown) chart.
27 Shows the percentage decline from the running peak for every column
28 in the dataset (assets and benchmark where present).
30 Args:
31 title: Chart title. Defaults to ``"Drawdowns"``.
33 Returns:
34 go.Figure: Interactive Plotly filled-area chart.
36 """
37 df = self._data.all
38 date_col = df.columns[0]
39 tickers = [c for c in df.columns if c != date_col]
40 colors = _ticker_colors(tickers)
42 prices = df.with_columns([(1.0 + pl.col(t)).cum_prod().alias(t) for t in tickers])
44 fig = go.Figure()
45 for ticker in tickers:
46 price_s = prices[ticker]
47 hwm = price_s.cum_max()
48 dd = ((price_s - hwm) / hwm).to_list()
50 fig.add_trace(
51 go.Scatter(
52 x=prices[date_col],
53 y=dd,
54 mode="lines",
55 fill="tozeroy",
56 fillcolor=_hex_to_rgba(colors[ticker], 0.3),
57 line={"color": colors[ticker], "width": 1.5},
58 name=ticker,
59 hovertemplate=f"{ticker}: %{{y:.2%}}",
60 )
61 )
63 fig.add_hline(y=0, line_width=1, line_color="gray")
64 _apply_base_layout(fig, title)
65 fig.update_yaxes(title_text="Drawdown", tickformat=".0%")
66 return fig
68 def drawdowns_periods(
69 self,
70 n: int = 5,
71 title: str = "Top Drawdown Periods",
72 asset: str | None = None,
73 ) -> go.Figure:
74 """Cumulative returns chart with the worst *n* drawdown periods shaded.
76 Identifies the *n* deepest drawdown periods and overlays coloured
77 rectangular shading on the cumulative returns line. One asset is
78 shown per call.
80 Args:
81 n: Number of worst drawdown periods to highlight. Defaults to 5.
82 title: Chart title. Defaults to ``"Top Drawdown Periods"``.
83 asset: Asset column name. Defaults to the first non-date column.
85 Returns:
86 go.Figure: Interactive Plotly figure.
88 """
89 df = self._data.all
90 date_col = df.columns[0]
91 tickers = [c for c in df.columns if c != date_col]
92 col = asset if asset in tickers else tickers[0]
94 price_series = (1.0 + df[col].cast(pl.Float64)).cum_prod()
95 price_list = price_series.to_list()
96 dates = df[date_col].to_list()
98 drawdown_periods = _compute_drawdown_periods(price_list, n)
100 dd_colors = px.colors.qualitative.Plotly
102 fig = go.Figure()
103 fig.add_trace(
104 go.Scatter(
105 x=dates,
106 y=price_list,
107 mode="lines",
108 name=col,
109 line={"color": "#1f77b4", "width": 2},
110 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{col}: %{{y:.2f}}x",
111 )
112 )
114 for i, period in enumerate(drawdown_periods):
115 start_date = dates[period["start_idx"]]
116 end_date = dates[min(period["end_idx"] + 1, len(dates) - 1)]
117 max_dd = period["max_drawdown"]
118 shade_color = _hex_to_rgba(dd_colors[i % len(dd_colors)], alpha=0.2)
120 fig.add_vrect(
121 x0=start_date,
122 x1=end_date,
123 fillcolor=shade_color,
124 line_width=0,
125 annotation_text=f"#{i + 1} {max_dd:.1%}",
126 annotation_position="top left",
127 annotation_font_size=10,
128 )
130 _apply_base_layout(fig, f"{title} — {col}")
131 fig.update_yaxes(title_text="Cumulative Return", tickformat=".2f")
132 return fig