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

1"""Result container for system/experiment outputs.""" 

2 

3from dataclasses import dataclass 

4from pathlib import Path 

5 

6import polars as pl 

7 

8from .exceptions import MuSchemaError 

9from .portfolio import Portfolio 

10 

11 

12@dataclass(frozen=True) 

13class Result: 

14 """Lightweight container for system outputs. 

15 

16 Attributes: 

17 portfolio: The portfolio constructed by a system/experiment. 

18 mu: Optional per-asset expected-returns surface used by some systems. 

19 """ 

20 

21 portfolio: Portfolio 

22 mu: pl.DataFrame | None = None 

23 

24 def __post_init__(self) -> None: 

25 """Validate that mu (when given) is a DataFrame covering every portfolio asset. 

26 

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) 

38 

39 def create_reports(self, output_dir: Path) -> None: 

40 """Generate CSV exports and interactive HTML plots for this result. 

41 

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" 

50 

51 data.mkdir(parents=True, exist_ok=True) 

52 plots.mkdir(parents=True, exist_ok=True) 

53 

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") 

58 

59 if self.mu is not None: 

60 self.mu.write_csv(file=data / "signal.csv") 

61 

62 self.portfolio.cashposition.write_csv(file=data / "position.csv") 

63 

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")