Coverage for src/jquantstats/result.py: 100%
39 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"""Result container for system/experiment outputs."""
3from dataclasses import dataclass
4from pathlib import Path
6import polars as pl
8from ._plots._backend import plot_backend
9from .exceptions import MuSchemaError
10from .portfolio import Portfolio
13@dataclass(frozen=True)
14class Result:
15 """Lightweight container for system outputs.
17 Attributes:
18 portfolio: The portfolio constructed by a system/experiment.
19 mu: Optional per-asset expected-returns surface used by some systems.
20 """
22 portfolio: Portfolio
23 mu: pl.DataFrame | None = None
25 def __post_init__(self) -> None:
26 """Validate that mu (when given) is a DataFrame covering every portfolio asset.
28 Raises:
29 TypeError: If ``mu`` is neither ``None`` nor a `polars.DataFrame`.
30 MuSchemaError: If ``mu`` lacks a column for one or more portfolio assets.
31 """
32 if self.mu is None:
33 return
34 if not isinstance(self.mu, pl.DataFrame):
35 raise TypeError(f"mu must be a polars DataFrame or None, got {type(self.mu).__name__}") # noqa: TRY003
36 missing = [asset for asset in self.portfolio.assets if asset not in self.mu.columns]
37 if missing:
38 raise MuSchemaError(missing)
40 def create_reports(self, output_dir: Path) -> None:
41 """Generate CSV exports and interactive HTML plots for this result.
43 Args:
44 output_dir: Destination directory where two subfolders will be created:
45 - data/: CSV exports of prices, profit, returns, positions, and signal (if mu present).
46 - plots/: Plotly HTML reports (snapshot, lead/lag IR, lagged performance,
47 smoothed holdings performance).
48 """
49 data = output_dir / "data"
50 plots = output_dir / "plots"
52 data.mkdir(parents=True, exist_ok=True)
53 plots.mkdir(parents=True, exist_ok=True)
55 self.portfolio.prices.write_csv(file=data / "prices.csv")
56 self.portfolio.profit.write_csv(file=data / "profit.csv")
57 self.portfolio.returns.write_csv(file=data / "returns.csv")
58 self.portfolio.tilt_timing_decomp.write_csv(file=data / "tilt_timing_decomp.csv")
60 if self.mu is not None:
61 self.mu.write_csv(file=data / "signal.csv")
63 self.portfolio.cashposition.write_csv(file=data / "position.csv")
65 # `write_html` is a Plotly-only method, so these charts must be Plotly
66 # figures whatever `set_plot_backend` has been told; a matplotlib figure
67 # would raise AttributeError here.
68 with plot_backend("plotly"):
69 fig = self.portfolio.plots.snapshot()
70 fig.write_html(file=plots / "snapshot.html", auto_open=False, include_plotlyjs="cdn")
71 fig = self.portfolio.plots.lead_lag_ir_plot()
72 fig.write_html(file=plots / "lag_ir.html", auto_open=False, include_plotlyjs="cdn")
73 fig = self.portfolio.plots.lagged_performance_plot()
74 fig.write_html(file=plots / "lagged_perf.html", auto_open=False, include_plotlyjs="cdn")
75 fig = self.portfolio.plots.smoothed_holdings_performance_plot()
76 fig.write_html(file=plots / "smooth_perf.html", auto_open=False, include_plotlyjs="cdn")