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

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

2 

3from dataclasses import dataclass 

4from pathlib import Path 

5 

6import polars as pl 

7 

8from ._plots._backend import plot_backend 

9from .exceptions import MuSchemaError 

10from .portfolio import Portfolio 

11 

12 

13@dataclass(frozen=True) 

14class Result: 

15 """Lightweight container for system outputs. 

16 

17 Attributes: 

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

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

20 """ 

21 

22 portfolio: Portfolio 

23 mu: pl.DataFrame | None = None 

24 

25 def __post_init__(self) -> None: 

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

27 

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) 

39 

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

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

42 

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" 

51 

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

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

54 

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

59 

60 if self.mu is not None: 

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

62 

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

64 

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