Coverage for src/cvx/simulator/_analytics.py: 100%

22 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 13:14 +0000

1# Copyright 2023 Stanford University Convex Optimization Group 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14"""jquantstats-backed performance analytics for the Portfolio class. 

15 

16This module houses :class:`PortfolioAnalytics`, a mixin that supplies the 

17statistics, plotting, and reporting surface layered on top of a portfolio's 

18net asset value. Keeping it separate from the core 

19:class:`~cvx.simulator.portfolio.Portfolio` data model isolates the dependency 

20on :mod:`jquantstats` and keeps each module focused on a single concern. 

21""" 

22 

23from __future__ import annotations 

24 

25from typing import TYPE_CHECKING, Any 

26 

27from jquantstats.data import Data 

28 

29if TYPE_CHECKING: 

30 import pandas as pd 

31 

32 

33class PortfolioAnalytics: 

34 """Mixin providing performance statistics, plots, and reports for a portfolio. 

35 

36 The mixin expects the host class to expose a ``nav`` :class:`pandas.Series` 

37 property. From that NAV it lazily builds and caches a 

38 :class:`jquantstats.data.Data` object (in :meth:`_build_data`) and exposes 

39 the ``stats``/``plots``/``reports`` surface plus the :meth:`sharpe` and 

40 :meth:`snapshot` convenience helpers on top of it. 

41 """ 

42 

43 if TYPE_CHECKING: 

44 # Supplied by the host dataclass (Portfolio); declared here purely so the 

45 # type checker can resolve the attributes the mixin relies on. 

46 _data: Data 

47 

48 @property 

49 def nav(self) -> pd.Series: 

50 """Net asset value of the portfolio over time (provided by the host).""" 

51 ... 

52 

53 def _build_data(self) -> None: 

54 """Build and cache the derived quantstats Data from the portfolio NAV. 

55 

56 This constructs the returns-based :class:`~jquantstats.data.Data` object 

57 used by the reporting and statistics helpers and stores it on the frozen 

58 instance's ``_data`` field. It is invoked from ``__post_init__`` after 

59 the input validation has passed. 

60 """ 

61 frame = self.nav.pct_change().to_frame().reset_index() 

62 frame.columns = ["Date", frame.columns[1]] 

63 d = Data.from_returns(returns=frame) 

64 

65 object.__setattr__(self, "_data", d) 

66 

67 @property 

68 def stats(self) -> Any: 

69 """Get statistical analysis data for the portfolio. 

70 

71 This property provides access to various statistical metrics calculated 

72 for the portfolio, such as Sharpe ratio, volatility, drawdowns, etc. 

73 

74 Returns: 

75 ------- 

76 object 

77 An object containing various statistical metrics for the portfolio 

78 

79 Notes: 

80 ----- 

81 The statistics are calculated by the underlying jquantstats library 

82 and are based on the portfolio's NAV time series. 

83 

84 """ 

85 return self._data.stats 

86 

87 @property 

88 def plots(self) -> Any: 

89 """Get visualization tools for the portfolio. 

90 

91 This property provides access to various plotting functions for visualizing 

92 the portfolio's performance, returns, drawdowns, etc. 

93 

94 Returns: 

95 ------- 

96 object 

97 An object containing various plotting methods for the portfolio 

98 

99 Notes: 

100 ----- 

101 The plotting functions are provided by the underlying jquantstats library 

102 and operate on the portfolio's NAV time series. 

103 

104 """ 

105 return self._data.plots 

106 

107 @property 

108 def reports(self) -> Any: 

109 """Get reporting tools for the portfolio. 

110 

111 This property provides access to various reporting functions for generating 

112 performance reports, risk metrics, and other analytics for the portfolio. 

113 

114 Returns: 

115 ------- 

116 object 

117 An object containing various reporting methods for the portfolio 

118 

119 Notes: 

120 ----- 

121 The reporting functions are provided by the underlying jquantstats library 

122 and operate on the portfolio's NAV time series. 

123 

124 """ 

125 return self._data.reports 

126 

127 def sharpe(self, periods: int | None = None) -> float: 

128 """Calculate the Sharpe ratio for the portfolio. 

129 

130 The Sharpe ratio is a measure of risk-adjusted return, calculated as 

131 the portfolio's excess return divided by its volatility. 

132 

133 Parameters 

134 ---------- 

135 periods : int, optional 

136 The number of periods per year for annualization. 

137 For daily data, use 252; for weekly data, use 52; for monthly data, use 12. 

138 If None, no annualization is performed. 

139 

140 Returns: 

141 ------- 

142 float 

143 The Sharpe ratio of the portfolio 

144 

145 Notes: 

146 ----- 

147 The Sharpe ratio is calculated using the portfolio's NAV time series. 

148 A higher Sharpe ratio indicates better risk-adjusted performance. 

149 

150 """ 

151 return float(self.stats.sharpe(periods=periods)["NAV"]) 

152 

153 def snapshot(self, title: str = "Portfolio Summary", log_scale: bool = True) -> Any: 

154 """Generate and display a snapshot of the portfolio summary. 

155 

156 This method creates a visual representation of the portfolio summary 

157 using the associated plot functionalities. The snapshot can be 

158 configured with a title and whether to use a logarithmic scale. 

159 

160 Args: 

161 title: A string specifying the title of the snapshot. 

162 Default is "Portfolio Summary". 

163 log_scale: A boolean indicating whether to display the plot 

164 using a logarithmic scale. Default is True. 

165 

166 Returns: 

167 The generated plot object representing the portfolio snapshot. 

168 

169 """ 

170 return self.plots.snapshot(title=title, log_scale=log_scale)