Coverage for src / jquantstats / _plots.py: 100%
51 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 02:21 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 02:21 +0000
1from __future__ import annotations
3import dataclasses
4from typing import TYPE_CHECKING
6import plotly.express as px
7import plotly.graph_objects as go
8import polars as pl
9from plotly.subplots import make_subplots
11if TYPE_CHECKING:
12 from ._data import Data
15def _plot_performance_dashboard(returns: pl.DataFrame, log_scale: bool = False) -> go.Figure:
16 def hex_to_rgba(hex_color: str, alpha: float = 0.5) -> str:
17 hex_color = hex_color.lstrip("#")
18 r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
19 return f"rgba({r}, {g}, {b}, {alpha})"
21 # Get the date column name from the first column of the DataFrame
22 date_col = returns.columns[0]
24 # Get the tickers (all columns except the date column)
25 tickers = [col for col in returns.columns if col != date_col]
27 # Calculate cumulative returns (prices)
28 prices = returns.with_columns([((1 + pl.col(ticker)).cum_prod()).alias(f"{ticker}_price") for ticker in tickers])
30 palette = px.colors.qualitative.Plotly
31 colors = {ticker: palette[i % len(palette)] for i, ticker in enumerate(tickers)}
32 colors.update({f"{ticker}_light": hex_to_rgba(colors[ticker]) for ticker in tickers})
34 # Resample to monthly returns
35 monthly_returns = returns.group_by_dynamic(
36 index_column=date_col, every="1mo", period="1mo", closed="right", label="right"
37 ).agg([((pl.col(ticker) + 1.0).product() - 1.0).alias(ticker) for ticker in tickers])
39 # Create subplot grid with domain for stats table
40 fig = make_subplots(
41 rows=3,
42 cols=1,
43 shared_xaxes=True,
44 row_heights=[0.5, 0.25, 0.25],
45 subplot_titles=["Cumulative Returns", "Drawdowns", "Monthly Returns"],
46 vertical_spacing=0.05,
47 )
49 # --- Row 1: Cumulative Returns
50 for ticker in tickers:
51 price_col = f"{ticker}_price"
52 fig.add_trace(
53 go.Scatter(
54 x=prices[date_col],
55 y=prices[price_col],
56 mode="lines",
57 name=ticker,
58 legendgroup=ticker,
59 line={"color": colors[ticker], "width": 2},
60 hovertemplate=f"<b>%{{x|%b %Y}}</b><br>{ticker}: %{{y:.2f}}x",
61 showlegend=True,
62 ),
63 row=1,
64 col=1,
65 )
67 # --- Row 2: Drawdowns
68 for ticker in tickers:
69 price_col = f"{ticker}_price"
70 # Calculate drawdowns using polars
71 price_series = prices[price_col]
72 cummax = prices.select(pl.col(price_col).cum_max().alias("cummax"))
73 dd_values = ((price_series - cummax["cummax"]) / cummax["cummax"]).to_list()
75 fig.add_trace(
76 go.Scatter(
77 x=prices[date_col],
78 y=dd_values,
79 mode="lines",
80 fill="tozeroy",
81 fillcolor=colors[f"{ticker}_light"],
82 line={"color": colors[ticker], "width": 1},
83 name=ticker,
84 legendgroup=ticker,
85 hovertemplate=f"{ticker} Drawdown: %{{y:.2%}}",
86 showlegend=False,
87 ),
88 row=2,
89 col=1,
90 )
92 fig.add_hline(y=0, line_width=1, line_color="gray", row=2, col=1)
94 # --- Row 3: Monthly Returns
95 for ticker in tickers:
96 # Get monthly returns values as a list for coloring
97 monthly_values = monthly_returns[ticker].to_list()
99 # If there's only one ticker, use green for positive returns and red for negative returns
100 if len(tickers) == 1:
101 bar_colors = ["green" if val > 0 else "red" for val in monthly_values]
102 else:
103 bar_colors = [colors[ticker] if val > 0 else colors[f"{ticker}_light"] for val in monthly_values]
105 fig.add_trace(
106 go.Bar(
107 x=monthly_returns[date_col],
108 y=monthly_returns[ticker],
109 name=ticker,
110 legendgroup=ticker,
111 marker={
112 "color": bar_colors,
113 "line": {"width": 0},
114 },
115 opacity=0.8,
116 hovertemplate=f"{ticker} Monthly Return: %{{y:.2%}}",
117 showlegend=False,
118 ),
119 row=3,
120 col=1,
121 )
123 # Layout
124 fig.update_layout(
125 title=f"{' vs '.join(tickers)} Performance Dashboard",
126 height=1200,
127 hovermode="x unified",
128 plot_bgcolor="white",
129 legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "right", "x": 1},
130 xaxis={
131 "rangeselector": {
132 "buttons": [
133 {"count": 6, "label": "6m", "step": "month", "stepmode": "backward"},
134 {"count": 1, "label": "1y", "step": "year", "stepmode": "backward"},
135 {"count": 3, "label": "3y", "step": "year", "stepmode": "backward"},
136 {"step": "year", "stepmode": "todate", "label": "YTD"},
137 {"step": "all", "label": "All"},
138 ]
139 },
140 "rangeslider": {"visible": False},
141 "type": "date",
142 },
143 )
145 fig.update_yaxes(title_text="Cumulative Return", row=1, col=1, tickformat=".2f")
146 fig.update_yaxes(title_text="Drawdown", row=2, col=1, tickformat=".0%")
147 fig.update_yaxes(title_text="Monthly Return", row=3, col=1, tickformat=".0%")
149 fig.update_xaxes(showgrid=True, gridwidth=0.5, gridcolor="lightgrey")
150 fig.update_yaxes(showgrid=True, gridwidth=0.5, gridcolor="lightgrey")
152 if log_scale:
153 fig.update_yaxes(type="log", row=1, col=1)
155 return fig
158@dataclasses.dataclass(frozen=True)
159class Plots:
160 """Visualization tools for financial returns data.
162 This class provides methods for creating various plots and visualizations
163 of financial returns data, including:
165 - Returns bar charts
166 - Portfolio performance snapshots
167 - Monthly returns heatmaps
169 The class is designed to work with the _Data class and uses Plotly
170 for creating interactive visualizations.
172 Attributes:
173 data: The _Data object containing returns and benchmark data to visualize.
175 """
177 data: Data
179 def plot_snapshot(self, title: str = "Portfolio Summary", log_scale: bool = False) -> go.Figure:
180 """Create a comprehensive dashboard with multiple plots for portfolio analysis.
182 This function generates a three-panel plot showing:
183 1. Cumulative returns over time
184 2. Drawdowns over time
185 3. Daily returns over time
187 This provides a complete visual summary of portfolio performance.
189 Args:
190 title (str, optional): Title of the plot. Defaults to "Portfolio Summary".
191 compounded (bool, optional): Whether to use compounded returns. Defaults to True.
192 log_scale (bool, optional): Whether to use logarithmic scale for cumulative returns.
193 Defaults to False.
195 Returns:
196 go.Figure: A Plotly figure object containing the dashboard.
198 Example:
199 >>> import polars as pl
200 >>> from jquantstats.api import build_data
201 >>> # minimal demo dataset with a Date column and one asset
202 >>> returns = pl.DataFrame({
203 ... "Date": ["2023-01-01", "2023-01-02", "2023-01-03"],
204 ... "Asset": [0.01, -0.02, 0.03],
205 ... }).with_columns(pl.col("Date").str.to_date())
206 >>> data = build_data(returns=returns)
207 >>> fig = data.plots.plot_snapshot(title="My Portfolio Performance")
208 >>> # Optional: display the interactive figure
209 >>> fig.show() # doctest: +SKIP
211 """
212 fig = _plot_performance_dashboard(returns=self.data.all, log_scale=log_scale)
213 return fig