Coverage for cvx/simulator/builder.py: 100%

75 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2025-01-10 14:11 +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. 

14from __future__ import annotations 

15 

16from dataclasses import dataclass 

17from typing import Generator 

18 

19import numpy as np 

20import pandas as pd 

21 

22from .portfolio import Portfolio 

23from .state import State 

24from .utils.interpolation import valid 

25from .utils.rescale import returns2prices 

26 

27 

28@dataclass 

29class Builder: 

30 """ 

31 The Builder is an auxiliary class used to build portfolios. 

32 It overloads the __iter__ method to allow the class to iterate over 

33 the timestamps for which the portfolio data is available. 

34 

35 In each iteration we can update the portfolio by setting either 

36 the weights, the position or the cash position. 

37 

38 After the iteration has been completed we build a Portfolio object 

39 by calling the build method. 

40 """ 

41 

42 prices: pd.DataFrame 

43 initial_aum: float = 1e6 

44 

45 _state: State = None 

46 _units: pd.DataFrame = None 

47 _aum: pd.Series = None 

48 

49 def __post_init__(self) -> None: 

50 """ 

51 The __post_init__ method is a special method of initialized instances 

52 of the _Builder class and is called after initialization. 

53 It sets the initial amount of cash in the portfolio to be equal to the input initial_cash parameter. 

54 

55 The method takes no input parameter. It initializes the cash attribute in the internal 

56 _State object with the initial amount of cash in the portfolio, self.initial_cash. 

57 

58 Note that this method is often used in Python classes for additional initialization routines 

59 that can only be performed after the object is fully initialized. __post_init__ 

60 is called automatically after the object initialization. 

61 """ 

62 

63 # assert isinstance(self.prices, pd.DataFrame) 

64 assert self.prices.index.is_monotonic_increasing 

65 assert self.prices.index.is_unique 

66 

67 self._state = State() 

68 

69 self._units = pd.DataFrame( 

70 index=self.prices.index, 

71 columns=self.prices.columns, 

72 data=np.nan, 

73 dtype=float, 

74 ) 

75 

76 self._aum = pd.Series(index=self.prices.index, dtype=float) 

77 

78 self._state.aum = self.initial_aum 

79 

80 @property 

81 def valid(self): 

82 """ 

83 Analyse the validity of the data 

84 Do it column by column of the prices 

85 """ 

86 return self.prices.apply(valid) 

87 

88 @property 

89 def intervals(self): 

90 """ 

91 Find for each column the first and the last valid index 

92 """ 

93 return self.prices.apply( 

94 lambda ts: pd.Series({"first": ts.first_valid_index(), "last": ts.last_valid_index()}) 

95 ).transpose() 

96 

97 @property 

98 def index(self) -> pd.DatetimeIndex: 

99 """A property that returns the index of the portfolio, 

100 which are the timestamps for which the portfolio data is available. 

101 

102 Returns: pd.Index: A pandas index representing the 

103 time period for which the portfolio data is available. 

104 """ 

105 return pd.DatetimeIndex(self.prices.index) 

106 

107 @property 

108 def current_prices(self) -> np.array: 

109 """ 

110 Get the current prices from the state 

111 """ 

112 return self._state.prices[self._state.assets].values 

113 

114 def __iter__(self) -> Generator[tuple[pd.DatetimeIndex, State]]: 

115 """ 

116 The __iter__ method allows the object to be iterated over in a for loop, 

117 yielding time and the current state of the portfolio. 

118 The method yields a list of dates seen so far and returns a tuple 

119 containing the list of dates and the current portfolio state. 

120 

121 Yield: 

122 

123 time: a pandas DatetimeIndex object containing the dates seen so far. 

124 state: the current state of the portfolio, 

125 

126 taking into account the stock prices at each interval. 

127 """ 

128 for t in self.index: 

129 # update the current prices for the portfolio 

130 self._state.prices = self.prices.loc[t] 

131 

132 # update the current time for the state 

133 self._state.time = t 

134 

135 # yield the vector of times seen so far and the current state 

136 yield self.index[self.index <= t], self._state 

137 

138 @property 

139 def position(self) -> pd.Series: 

140 """ 

141 The position property returns the current position of the portfolio. 

142 It returns a pandas Series object containing the current position of the portfolio. 

143 

144 Returns: pd.Series: a pandas Series object containing the current position of the portfolio. 

145 """ 

146 return self._units.loc[self._state.time] 

147 

148 @position.setter 

149 def position(self, position: pd.Series) -> None: 

150 """ 

151 The position property returns the current position of the portfolio. 

152 It returns a pandas Series object containing the current position of the portfolio. 

153 

154 Returns: pd.Series: a pandas Series object containing the current position of the portfolio. 

155 """ 

156 self._units.loc[self._state.time, self._state.assets] = position 

157 self._state.position = position 

158 

159 @property 

160 def cashposition(self): 

161 """ 

162 The cashposition property returns the current cash position of the portfolio. 

163 """ 

164 return self.position * self.current_prices 

165 

166 @property 

167 def units(self): 

168 """ 

169 The units property returns the frame of holdings of the portfolio. 

170 Useful mainly for testing 

171 """ 

172 return self._units 

173 

174 @cashposition.setter 

175 def cashposition(self, cashposition: pd.Series) -> None: 

176 """ 

177 The cashposition property sets the current cash position of the portfolio. 

178 """ 

179 self.position = cashposition / self.current_prices 

180 

181 def build(self): 

182 """A function that creates a new instance of the EquityPortfolio 

183 class based on the internal state of the Portfolio builder object. 

184 

185 Returns: EquityPortfolio: A new instance of the EquityPortfolio class 

186 with the attributes (prices, units, initial_cash, trading_cost_model) as specified in the Portfolio builder. 

187 

188 Notes: The function simply creates a new instance of the EquityPortfolio 

189 class with the attributes (prices, units, initial_cash, trading_cost_model) equal 

190 to the corresponding attributes in the Portfolio builder object. 

191 The resulting EquityPortfolio object will have the same state as the Portfolio builder from which it was built. 

192 """ 

193 

194 return Portfolio(prices=self.prices, units=self.units, aum=self.aum) 

195 

196 @property 

197 def weights(self) -> np.array: 

198 """ 

199 Get the current weights from the state 

200 """ 

201 return self._state.weights[self._state.assets].values 

202 

203 @weights.setter 

204 def weights(self, weights: np.array) -> None: 

205 """ 

206 The weights property sets the current weights of the portfolio. 

207 We convert the weights to positions using the current prices and the NAV 

208 """ 

209 self.position = self._state.nav * weights / self.current_prices 

210 

211 @property 

212 def aum(self): 

213 """ 

214 The aum property returns the current AUM of the portfolio. 

215 """ 

216 return self._aum 

217 

218 @aum.setter 

219 def aum(self, aum): 

220 """ 

221 The aum property sets the current AUM of the portfolio. 

222 """ 

223 self._aum[self._state.time] = aum 

224 self._state.aum = aum 

225 

226 @classmethod 

227 def from_returns(cls, returns): 

228 """Build Futures Portfolio from returns""" 

229 # compute artificial prices (but scaled such their returns are correct) 

230 

231 prices = returns2prices(returns) 

232 return cls(prices=prices)