Coverage for src/jquantstats/_plots/_protocol.py: 100%

6 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-23 06:13 +0000

1"""Protocols describing the minimal interfaces required by the _plots subpackage.""" 

2 

3from __future__ import annotations 

4 

5from typing import Protocol, runtime_checkable 

6 

7import polars as pl 

8 

9from jquantstats._cost_model import CostModel 

10from jquantstats._protocol import DataLike 

11 

12__all__ = ["DataLike", "PortfolioLike", "PortfolioStatsLike"] 

13 

14 

15class PortfolioStatsLike(Protocol): # pragma: no cover 

16 """Structural interface for the stats facade used by `PortfolioPlots`.""" 

17 

18 def sharpe(self) -> dict[str, float]: 

19 """Annualised Sharpe ratio per asset column.""" 

20 ... 

21 

22 def rolling_sharpe( 

23 self, 

24 rolling_period: int = 126, 

25 periods_per_year: int | float | None = None, 

26 ) -> pl.DataFrame: 

27 """Rolling Sharpe ratio series.""" 

28 ... 

29 

30 def rolling_volatility( 

31 self, 

32 rolling_period: int = 126, 

33 periods_per_year: int | float | None = None, 

34 annualize: bool = True, 

35 ) -> pl.DataFrame: 

36 """Rolling volatility series.""" 

37 ... 

38 

39 def annual_breakdown(self) -> pl.DataFrame: 

40 """Per-year metric breakdown.""" 

41 ... 

42 

43 

44@runtime_checkable 

45class PortfolioLike(Protocol): # pragma: no cover 

46 """Structural interface required by the `PortfolioPlots` class. 

47 

48 Any object satisfying this protocol can be passed as ``portfolio`` without a 

49 concrete dependency on `Portfolio`. 

50 """ 

51 

52 @property 

53 def prices(self) -> pl.DataFrame: 

54 """Price (holding) DataFrame.""" 

55 ... 

56 

57 @property 

58 def aum(self) -> float: 

59 """Assets under management.""" 

60 ... 

61 

62 @property 

63 def cost_model(self) -> CostModel: 

64 """Trading cost model.""" 

65 ... 

66 

67 @property 

68 def nav_accumulated(self) -> pl.DataFrame: 

69 """Accumulated NAV series.""" 

70 ... 

71 

72 @property 

73 def tilt(self) -> PortfolioLike: 

74 """Tilt component portfolio.""" 

75 ... 

76 

77 @property 

78 def timing(self) -> PortfolioLike: 

79 """Timing component portfolio.""" 

80 ... 

81 

82 @property 

83 def net_cost_nav(self) -> pl.DataFrame: 

84 """Net-of-cost accumulated NAV series.""" 

85 ... 

86 

87 @property 

88 def drawdown(self) -> pl.DataFrame: 

89 """Drawdown series.""" 

90 ... 

91 

92 @property 

93 def assets(self) -> list[str]: 

94 """Asset names.""" 

95 ... 

96 

97 @property 

98 def monthly(self) -> pl.DataFrame: 

99 """Monthly returns grouped by year and month.""" 

100 ... 

101 

102 @property 

103 def profits(self) -> pl.DataFrame: 

104 """Per-period profit series.""" 

105 ... 

106 

107 @property 

108 def stats(self) -> PortfolioStatsLike: 

109 """Statistics facade (rolling_sharpe, rolling_volatility, annual_breakdown, sharpe).""" 

110 ... 

111 

112 def lag(self, n: int) -> PortfolioLike: 

113 """Return a lagged copy of this portfolio.""" 

114 ... 

115 

116 def smoothed_holding(self, n: int) -> PortfolioLike: 

117 """Return a smoothed-holdings copy of this portfolio.""" 

118 ... 

119 

120 def trading_cost_impact(self, max_bps: int = 20) -> pl.DataFrame: 

121 """Return a DataFrame of Sharpe vs. one-way trading costs.""" 

122 ... 

123 

124 def correlation(self, frame: pl.DataFrame, name: str = "portfolio") -> pl.DataFrame: 

125 """Return the correlation matrix including the portfolio profit series.""" 

126 ...