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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-07 14:28 +0000
1"""Utility methods for Portfolio objects."""
3from __future__ import annotations
5import dataclasses
7import polars as pl
9from ._data import DataUtils
10from ._protocol import PortfolioLike
12__all__ = ["PortfolioUtils"]
15@dataclasses.dataclass(frozen=True)
16class PortfolioUtils:
17 """Utility transforms and conversions for Portfolio objects.
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``.
23 Attributes:
24 portfolio: Any object satisfying the
25 :class:`~jquantstats._utils._protocol.PortfolioLike` protocol —
26 typically a :class:`~jquantstats.portfolio.Portfolio` instance.
28 """
30 portfolio: PortfolioLike
32 def __repr__(self) -> str:
33 """Return a string representation of the PortfolioUtils object."""
34 return f"PortfolioUtils(assets={self.portfolio.assets})"
36 def _du(self) -> DataUtils:
37 """Return a DataUtils instance backed by this portfolio's data bridge."""
38 return DataUtils(self.portfolio.data)
40 # ── delegated API (mirrors DataUtils) ─────────────────────────────────────
42 def to_prices(self, base: float = 1e5) -> pl.DataFrame:
43 """Convert portfolio returns to a cumulative price series.
45 See :meth:`~jquantstats._utils._data.DataUtils.to_prices` for full
46 documentation.
48 Args:
49 base: Starting value for the price series. Defaults to ``1e5``.
51 Returns:
52 DataFrame with date column (if present) and one price column per asset.
54 """
55 return self._du().to_prices(base=base)
57 def to_log_returns(self) -> pl.DataFrame:
58 """Convert portfolio returns to log returns: ``ln(1 + r)``.
60 See :meth:`~jquantstats._utils._data.DataUtils.to_log_returns` for
61 full documentation.
63 Returns:
64 DataFrame of log returns.
66 """
67 return self._du().to_log_returns()
69 def log_returns(self) -> pl.DataFrame:
70 """Alias for :meth:`to_log_returns`.
72 Returns:
73 DataFrame of log returns.
75 """
76 return self._du().log_returns()
78 def rebase(self, base: float = 100.0) -> pl.DataFrame:
79 """Normalise the portfolio's returns as a price series starting at *base*.
81 See :meth:`~jquantstats._utils._data.DataUtils.rebase` for full
82 documentation.
84 Args:
85 base: Target starting value. Defaults to ``100.0``.
87 Returns:
88 DataFrame with price columns anchored to *base* at t = 0.
90 """
91 return self._du().rebase(base=base)
93 def group_returns(self, period: str = "1mo", compounded: bool = True) -> pl.DataFrame:
94 """Aggregate portfolio returns by a calendar period.
96 See :meth:`~jquantstats._utils._data.DataUtils.group_returns` for
97 full documentation.
99 Args:
100 period: Aggregation period. Defaults to ``"1mo"`` (monthly).
101 compounded: Whether to compound returns. Defaults to ``True``.
103 Returns:
104 DataFrame with one row per period and one column per asset.
106 """
107 return self._du().group_returns(period=period, compounded=compounded)
109 def aggregate_returns(self, period: str = "1mo", compounded: bool = True) -> pl.DataFrame:
110 """Alias for :meth:`group_returns`.
112 Args:
113 period: Aggregation period. Defaults to ``"1mo"`` (monthly).
114 compounded: Whether to compound returns. Defaults to ``True``.
116 Returns:
117 DataFrame with one row per period and one column per asset.
119 """
120 return self._du().aggregate_returns(period=period, compounded=compounded)
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.
125 See :meth:`~jquantstats._utils._data.DataUtils.to_excess_returns`
126 for full documentation.
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``.
132 Returns:
133 DataFrame of excess returns.
135 """
136 return self._du().to_excess_returns(rf=rf, nperiods=nperiods)
138 def exponential_stdev(self, window: int = 30, is_halflife: bool = False) -> pl.DataFrame:
139 """Compute exponentially weighted standard deviation of portfolio returns.
141 See :meth:`~jquantstats._utils._data.DataUtils.exponential_stdev`
142 for full documentation.
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``.
149 Returns:
150 DataFrame of rolling EWMA standard deviations.
152 """
153 return self._du().exponential_stdev(window=window, is_halflife=is_halflife)