Coverage for src/jquantstats/result.py: 100%
37 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-23 06:13 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-23 06:13 +0000
1"""Result container for system/experiment outputs."""
3from dataclasses import dataclass
4from pathlib import Path
6import polars as pl
8from .exceptions import MuSchemaError
9from .portfolio import Portfolio
12@dataclass(frozen=True)
13class Result:
14 """Lightweight container for system outputs.
16 Attributes:
17 portfolio: The portfolio constructed by a system/experiment.
18 mu: Optional per-asset expected-returns surface used by some systems.
19 """
21 portfolio: Portfolio
22 mu: pl.DataFrame | None = None
24 def __post_init__(self) -> None:
25 """Validate that mu (when given) is a DataFrame covering every portfolio asset.
27 Raises:
28 TypeError: If ``mu`` is neither ``None`` nor a `polars.DataFrame`.
29 MuSchemaError: If ``mu`` lacks a column for one or more portfolio assets.
30 """
31 if self.mu is None:
32 return
33 if not isinstance(self.mu, pl.DataFrame):
34 raise TypeError(f"mu must be a polars DataFrame or None, got {type(self.mu).__name__}") # noqa: TRY003
35 missing = [asset for asset in self.portfolio.assets if asset not in self.mu.columns]
36 if missing:
37 raise MuSchemaError(missing)
39 def create_reports(self, output_dir: Path) -> None:
40 """Generate CSV exports and interactive HTML plots for this result.
42 Args:
43 output_dir: Destination directory where two subfolders will be created:
44 - data/: CSV exports of prices, profit, returns, positions, and signal (if mu present).
45 - plots/: Plotly HTML reports (snapshot, lead/lag IR, lagged performance,
46 smoothed holdings performance).
47 """
48 data = output_dir / "data"
49 plots = output_dir / "plots"
51 data.mkdir(parents=True, exist_ok=True)
52 plots.mkdir(parents=True, exist_ok=True)
54 self.portfolio.prices.write_csv(file=data / "prices.csv")
55 self.portfolio.profit.write_csv(file=data / "profit.csv")
56 self.portfolio.returns.write_csv(file=data / "returns.csv")
57 self.portfolio.tilt_timing_decomp.write_csv(file=data / "tilt_timing_decomp.csv")
59 if self.mu is not None:
60 self.mu.write_csv(file=data / "signal.csv")
62 self.portfolio.cashposition.write_csv(file=data / "position.csv")
64 fig = self.portfolio.plots.snapshot()
65 fig.write_html(file=plots / "snapshot.html", auto_open=False, include_plotlyjs="cdn")
66 fig = self.portfolio.plots.lead_lag_ir_plot()
67 fig.write_html(file=plots / "lag_ir.html", auto_open=False, include_plotlyjs="cdn")
68 fig = self.portfolio.plots.lagged_performance_plot()
69 fig.write_html(file=plots / "lagged_perf.html", auto_open=False, include_plotlyjs="cdn")
70 fig = self.portfolio.plots.smoothed_holdings_performance_plot()
71 fig.write_html(file=plots / "smooth_perf.html", auto_open=False, include_plotlyjs="cdn")