Coverage for src / jquantstats / _utils / _portfolio.py: 100%

29 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-07 14:28 +0000

1"""Utility methods for Portfolio objects.""" 

2 

3from __future__ import annotations 

4 

5import dataclasses 

6 

7import polars as pl 

8 

9from ._data import DataUtils 

10from ._protocol import PortfolioLike 

11 

12__all__ = ["PortfolioUtils"] 

13 

14 

15@dataclasses.dataclass(frozen=True) 

16class PortfolioUtils: 

17 """Utility transforms and conversions for Portfolio objects. 

18 

19 Exposes the same API as :class:`~jquantstats._utils._data.DataUtils` 

20 but is initialised from a :class:`~jquantstats.portfolio.Portfolio` 

21 and routes all calls through ``portfolio.data``. 

22 

23 Attributes: 

24 portfolio: Any object satisfying the 

25 :class:`~jquantstats._utils._protocol.PortfolioLike` protocol — 

26 typically a :class:`~jquantstats.portfolio.Portfolio` instance. 

27 

28 """ 

29 

30 portfolio: PortfolioLike 

31 

32 def __repr__(self) -> str: 

33 """Return a string representation of the PortfolioUtils object.""" 

34 return f"PortfolioUtils(assets={self.portfolio.assets})" 

35 

36 def _du(self) -> DataUtils: 

37 """Return a DataUtils instance backed by this portfolio's data bridge.""" 

38 return DataUtils(self.portfolio.data) 

39 

40 # ── delegated API (mirrors DataUtils) ───────────────────────────────────── 

41 

42 def to_prices(self, base: float = 1e5) -> pl.DataFrame: 

43 """Convert portfolio returns to a cumulative price series. 

44 

45 See :meth:`~jquantstats._utils._data.DataUtils.to_prices` for full 

46 documentation. 

47 

48 Args: 

49 base: Starting value for the price series. Defaults to ``1e5``. 

50 

51 Returns: 

52 DataFrame with date column (if present) and one price column per asset. 

53 

54 """ 

55 return self._du().to_prices(base=base) 

56 

57 def to_log_returns(self) -> pl.DataFrame: 

58 """Convert portfolio returns to log returns: ``ln(1 + r)``. 

59 

60 See :meth:`~jquantstats._utils._data.DataUtils.to_log_returns` for 

61 full documentation. 

62 

63 Returns: 

64 DataFrame of log returns. 

65 

66 """ 

67 return self._du().to_log_returns() 

68 

69 def log_returns(self) -> pl.DataFrame: 

70 """Alias for :meth:`to_log_returns`. 

71 

72 Returns: 

73 DataFrame of log returns. 

74 

75 """ 

76 return self._du().log_returns() 

77 

78 def rebase(self, base: float = 100.0) -> pl.DataFrame: 

79 """Normalise the portfolio's returns as a price series starting at *base*. 

80 

81 See :meth:`~jquantstats._utils._data.DataUtils.rebase` for full 

82 documentation. 

83 

84 Args: 

85 base: Target starting value. Defaults to ``100.0``. 

86 

87 Returns: 

88 DataFrame with price columns anchored to *base* at t = 0. 

89 

90 """ 

91 return self._du().rebase(base=base) 

92 

93 def group_returns(self, period: str = "1mo", compounded: bool = True) -> pl.DataFrame: 

94 """Aggregate portfolio returns by a calendar period. 

95 

96 See :meth:`~jquantstats._utils._data.DataUtils.group_returns` for 

97 full documentation. 

98 

99 Args: 

100 period: Aggregation period. Defaults to ``"1mo"`` (monthly). 

101 compounded: Whether to compound returns. Defaults to ``True``. 

102 

103 Returns: 

104 DataFrame with one row per period and one column per asset. 

105 

106 """ 

107 return self._du().group_returns(period=period, compounded=compounded) 

108 

109 def aggregate_returns(self, period: str = "1mo", compounded: bool = True) -> pl.DataFrame: 

110 """Alias for :meth:`group_returns`. 

111 

112 Args: 

113 period: Aggregation period. Defaults to ``"1mo"`` (monthly). 

114 compounded: Whether to compound returns. Defaults to ``True``. 

115 

116 Returns: 

117 DataFrame with one row per period and one column per asset. 

118 

119 """ 

120 return self._du().aggregate_returns(period=period, compounded=compounded) 

121 

122 def to_excess_returns(self, rf: float = 0.0, nperiods: int | None = None) -> pl.DataFrame: 

123 """Subtract a risk-free rate from portfolio returns. 

124 

125 See :meth:`~jquantstats._utils._data.DataUtils.to_excess_returns` 

126 for full documentation. 

127 

128 Args: 

129 rf: Annual risk-free rate as a decimal. Defaults to ``0.0``. 

130 nperiods: Periods per year for rate conversion. Defaults to ``None``. 

131 

132 Returns: 

133 DataFrame of excess returns. 

134 

135 """ 

136 return self._du().to_excess_returns(rf=rf, nperiods=nperiods) 

137 

138 def exponential_stdev(self, window: int = 30, is_halflife: bool = False) -> pl.DataFrame: 

139 """Compute exponentially weighted standard deviation of portfolio returns. 

140 

141 See :meth:`~jquantstats._utils._data.DataUtils.exponential_stdev` 

142 for full documentation. 

143 

144 Args: 

145 window: Span or half-life of the EWMA decay. Defaults to ``30``. 

146 is_halflife: Interpret *window* as half-life when ``True``. 

147 Defaults to ``False``. 

148 

149 Returns: 

150 DataFrame of rolling EWMA standard deviations. 

151 

152 """ 

153 return self._du().exponential_stdev(window=window, is_halflife=is_halflife)