Coverage for src / jquantstats / _utils / _portfolio.py: 100%
32 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-07 15:52 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-07 15:52 +0000
1"""Utility methods for Portfolio objects."""
3from __future__ import annotations
5import dataclasses
6from collections.abc import Callable
8import polars as pl
10from ._data import DataUtils
11from ._protocol import PortfolioLike
13__all__ = ["PortfolioUtils"]
16@dataclasses.dataclass(frozen=True)
17class PortfolioUtils:
18 """Utility transforms and conversions for Portfolio objects.
20 Exposes the same API as `DataUtils`
21 but is initialised from a `Portfolio`
22 and routes all calls through ``portfolio.data``.
24 Attributes:
25 portfolio: Any object satisfying the
26 `PortfolioLike` protocol —
27 typically a `Portfolio` instance.
29 """
31 portfolio: PortfolioLike
33 def __repr__(self) -> str:
34 """Return a string representation of the PortfolioUtils object."""
35 return f"PortfolioUtils(assets={self.portfolio.assets})"
37 def _du(self) -> DataUtils:
38 """Return a DataUtils instance backed by this portfolio's data bridge."""
39 return DataUtils(self.portfolio.data)
41 # ── delegated API (mirrors DataUtils) ─────────────────────────────────────
43 def to_prices(self, base: float = 1e5) -> pl.DataFrame:
44 """Convert portfolio returns to a cumulative price series.
46 See `to_prices` for full
47 documentation.
49 Args:
50 base: Starting value for the price series. Defaults to ``1e5``.
52 Returns:
53 DataFrame with date column (if present) and one price column per asset.
55 """
56 return self._du().to_prices(base=base)
58 def to_log_returns(self) -> pl.DataFrame:
59 """Convert portfolio returns to log returns: ``ln(1 + r)``.
61 See `to_log_returns` for
62 full documentation.
64 Returns:
65 DataFrame of log returns.
67 """
68 return self._du().to_log_returns()
70 def log_returns(self) -> pl.DataFrame:
71 """Alias for `to_log_returns`.
73 Returns:
74 DataFrame of log returns.
76 """
77 return self._du().log_returns()
79 def rebase(self, base: float = 100.0) -> pl.DataFrame:
80 """Normalise the portfolio's returns as a price series starting at *base*.
82 See `rebase` for full
83 documentation.
85 Args:
86 base: Target starting value. Defaults to ``100.0``.
88 Returns:
89 DataFrame with price columns anchored to *base* at t = 0.
91 """
92 return self._du().rebase(base=base)
94 def group_returns(self, period: str = "1mo", compounded: bool = True) -> pl.DataFrame:
95 """Aggregate portfolio returns by a calendar period.
97 See `group_returns` for
98 full documentation.
100 Args:
101 period: Aggregation period. Defaults to ``"1mo"`` (monthly).
102 compounded: Whether to compound returns. Defaults to ``True``.
104 Returns:
105 DataFrame with one row per period and one column per asset.
107 """
108 return self._du().group_returns(period=period, compounded=compounded)
110 def aggregate_returns(self, period: str = "1mo", compounded: bool = True) -> pl.DataFrame:
111 """Alias for `group_returns`.
113 Args:
114 period: Aggregation period. Defaults to ``"1mo"`` (monthly).
115 compounded: Whether to compound returns. Defaults to ``True``.
117 Returns:
118 DataFrame with one row per period and one column per asset.
120 """
121 return self._du().aggregate_returns(period=period, compounded=compounded)
123 def to_excess_returns(self, rf: float = 0.0, nperiods: int | None = None) -> pl.DataFrame:
124 """Subtract a risk-free rate from portfolio returns.
126 See `to_excess_returns`
127 for full documentation.
129 Args:
130 rf: Annual risk-free rate as a decimal. Defaults to ``0.0``.
131 nperiods: Periods per year for rate conversion. Defaults to ``None``.
133 Returns:
134 DataFrame of excess returns.
136 """
137 return self._du().to_excess_returns(rf=rf, nperiods=nperiods)
139 def to_volatility_adjusted_returns(
140 self,
141 window: int = 60,
142 vol_estimator: Callable[[pl.Expr], pl.Expr] | None = None,
143 ) -> pl.DataFrame:
144 """Convert portfolio returns to volatility-adjusted returns.
146 See `to_volatility_adjusted_returns` for full documentation.
148 Args:
149 window: Rolling lookback for volatility. Defaults to ``60``.
150 vol_estimator: A callable ``(pl.Expr) -> pl.Expr`` that
151 produces a volatility series. Defaults to ``None``
152 (uses ``rolling_std(window)``).
154 Returns:
155 DataFrame of volatility-adjusted returns.
157 """
158 return self._du().to_volatility_adjusted_returns(window=window, vol_estimator=vol_estimator)
160 def exponential_stdev(self, window: int = 30, is_halflife: bool = False) -> pl.DataFrame:
161 """Compute exponentially weighted standard deviation of portfolio returns.
163 See `exponential_stdev`
164 for full documentation.
166 Args:
167 window: Span or half-life of the EWMA decay. Defaults to ``30``.
168 is_halflife: Interpret *window* as half-life when ``True``.
169 Defaults to ``False``.
171 Returns:
172 DataFrame of rolling EWMA standard deviations.
174 """
175 return self._du().exponential_stdev(window=window, is_halflife=is_halflife)