Coverage for src/jquantstats/_plots/_data/_cumulative.py: 100%
70 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"""Cumulative-return and equity-curve line charts."""
3from __future__ import annotations
5import math
6from typing import TYPE_CHECKING
8import plotly.graph_objects as go
9import polars as pl
11from ._styling import _apply_base_layout, _apply_figsize, _ticker_colors
13if TYPE_CHECKING:
14 from jquantstats._protocol import DataLike
17class _CumulativePlotsMixin:
18 """Cumulative-return and equity-curve plots for :class:`DataPlots`."""
20 __slots__ = ()
22 _data: DataLike
24 def returns(self, title: str = "Cumulative Returns", log_scale: bool = False) -> go.Figure:
25 """Cumulative compounded returns over time.
27 Plots ``(1 + r).cumprod()`` for every column in the dataset (including
28 benchmark when present).
30 Args:
31 title: Chart title. Defaults to ``"Cumulative Returns"``.
32 log_scale: Use a logarithmic y-axis. Defaults to False.
34 Returns:
35 go.Figure: Interactive Plotly line chart.
37 """
38 df = self._data.all
39 date_col = df.columns[0]
40 tickers = [c for c in df.columns if c != date_col]
41 colors = _ticker_colors(tickers)
43 prices = df.with_columns([(1.0 + pl.col(t)).cum_prod().alias(t) for t in tickers])
45 fig = go.Figure()
46 for ticker in tickers:
47 fig.add_trace(
48 go.Scatter(
49 x=prices[date_col],
50 y=prices[ticker],
51 mode="lines",
52 name=ticker,
53 line={"color": colors[ticker], "width": 2},
54 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{ticker}: %{{y:.2f}}x",
55 )
56 )
58 _apply_base_layout(fig, title)
59 fig.update_yaxes(title_text="Cumulative Return", tickformat=".2f")
60 if log_scale:
61 fig.update_yaxes(type="log")
62 return fig
64 def compare(self, title: str = "Comparison vs Benchmark", figsize: tuple[int, int] | None = None) -> go.Figure:
65 """Compare cumulative returns of each asset against the benchmark.
67 Args:
68 title: Chart title. Defaults to ``"Comparison vs Benchmark"``.
69 figsize: Optional ``(width, height)`` in pixels.
71 Returns:
72 go.Figure: Interactive Plotly line chart.
74 Raises:
75 AttributeError: If no benchmark data is available.
77 """
78 benchmark_df = getattr(self._data, "benchmark", None)
79 if benchmark_df is None:
80 raise AttributeError("compare() requires benchmark data to be set") # noqa: TRY003
82 df = self._data.all
83 date_col = df.columns[0]
84 assets = list(self._data.returns.columns)
85 benchmarks = list(benchmark_df.columns)
87 series = assets + benchmarks
88 colors = _ticker_colors(series)
89 prices = df.with_columns([(1.0 + pl.col(col)).cum_prod().alias(col) for col in series])
91 fig = go.Figure()
92 for asset in assets:
93 fig.add_trace(
94 go.Scatter(
95 x=prices[date_col],
96 y=prices[asset],
97 mode="lines",
98 name=asset,
99 line={"color": colors[asset], "width": 2},
100 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{asset}: %{{y:.2f}}x",
101 )
102 )
103 for benchmark in benchmarks:
104 fig.add_trace(
105 go.Scatter(
106 x=prices[date_col],
107 y=prices[benchmark],
108 mode="lines",
109 name=benchmark,
110 line={"color": colors[benchmark], "width": 2.5, "dash": "dash"},
111 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{benchmark}: %{{y:.2f}}x",
112 )
113 )
115 _apply_base_layout(fig, title)
116 _apply_figsize(fig, figsize)
117 fig.update_yaxes(title_text="Cumulative Return", tickformat=".2f")
118 return fig
120 def log_returns(self, title: str = "Log Returns", figsize: tuple[int, int] | None = None) -> go.Figure:
121 """Cumulative log returns over time.
123 Plots ``log((1 + r).cumprod())`` — the natural log of the compounded
124 growth factor — which linearises exponential growth and makes
125 multi-asset comparisons on a common scale.
127 Args:
128 title: Chart title. Defaults to ``"Log Returns"``.
129 figsize: Optional ``(width, height)`` in pixels.
131 Returns:
132 go.Figure: Interactive Plotly line chart.
134 """
135 df = self._data.all
136 date_col = df.columns[0]
137 tickers = [c for c in df.columns if c != date_col]
138 colors = _ticker_colors(tickers)
140 log_prices = df.with_columns([(1.0 + pl.col(t)).cum_prod().log(math.e).alias(t) for t in tickers])
142 fig = go.Figure()
143 for ticker in tickers:
144 fig.add_trace(
145 go.Scatter(
146 x=log_prices[date_col],
147 y=log_prices[ticker],
148 mode="lines",
149 name=ticker,
150 line={"color": colors[ticker], "width": 2},
151 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{ticker}: %{{y:.4f}}",
152 )
153 )
155 _apply_base_layout(fig, title)
156 _apply_figsize(fig, figsize)
157 fig.update_yaxes(title_text="Log Return")
158 return fig
160 def earnings(
161 self,
162 start_balance: float = 1e5,
163 title: str = "Portfolio Earnings",
164 compounded: bool = True,
165 ) -> go.Figure:
166 """Dollar equity curve showing portfolio value over time.
168 Scales cumulative returns by *start_balance* so the y-axis reflects
169 an absolute portfolio value rather than a dimensionless growth factor.
171 Args:
172 start_balance: Starting portfolio value in currency units.
173 Defaults to 100 000.
174 title: Chart title. Defaults to ``"Portfolio Earnings"``.
175 compounded: Use compounded returns (``cumprod``). When False uses
176 cumulative sum. Defaults to True.
178 Returns:
179 go.Figure: Interactive Plotly line chart.
181 """
182 df = self._data.all
183 date_col = df.columns[0]
184 tickers = [c for c in df.columns if c != date_col]
185 colors = _ticker_colors(tickers)
187 if compounded:
188 equity = df.with_columns([(start_balance * (1.0 + pl.col(t)).cum_prod()).alias(t) for t in tickers])
189 else:
190 equity = df.with_columns([(start_balance * (1.0 + pl.col(t).cum_sum())).alias(t) for t in tickers])
192 fig = go.Figure()
193 for ticker in tickers:
194 fig.add_trace(
195 go.Scatter(
196 x=equity[date_col],
197 y=equity[ticker],
198 mode="lines",
199 name=ticker,
200 line={"color": colors[ticker], "width": 2},
201 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{ticker}: $%{{y:,.0f}}",
202 )
203 )
205 _apply_base_layout(fig, title)
206 fig.update_yaxes(
207 title_text=f"Portfolio Value (starting ${start_balance:,.0f})",
208 tickprefix="$",
209 tickformat=",.0f",
210 )
211 return fig