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

95 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 dataclasses import dataclass 

15from datetime import datetime 

16 

17import numpy as np 

18import pandas as pd 

19 

20 

21@dataclass() 

22class State: 

23 """ 

24 The state class represents the current state of a portfolio. 

25 It is updated within in a loop by the builder class. 

26 It has setter functions only for the aum, the cash, the position and the prices 

27 """ 

28 

29 _prices: pd.Series = None 

30 _position: pd.Series = None 

31 _trades: pd.Series = None 

32 _time: datetime = None 

33 _days: int = 0 

34 _profit: float = 0.0 

35 _aum: float = 0.0 

36 

37 @property 

38 def cash(self): 

39 """ 

40 The cash property returns the current amount of cash available in the portfolio. 

41 """ 

42 return self.nav - self.value 

43 

44 @cash.setter 

45 def cash(self, cash: float): 

46 """ 

47 The cash property updates the amount of cash available in the portfolio. 

48 """ 

49 self.aum = cash + self.value 

50 

51 @property 

52 def nav(self) -> float: 

53 """ 

54 The nav property computes the net asset value (NAV) of the portfolio, 

55 which is the sum of the current value of the 

56 portfolio as determined by the value property, 

57 and the current amount of cash available in the portfolio. 

58 """ 

59 # assert np.isclose(self.value + self.cash, self.aum), f"{self.value + self.cash} != {self.aum}" 

60 # return self.value + self.cash 

61 return self.aum 

62 

63 @property 

64 def value(self) -> float: 

65 """ 

66 The value property computes the value of the portfolio at the current 

67 time taking into account the current holdings and current prices. 

68 If the value cannot be computed due to missing positions 

69 (they might be still None), zero is returned instead. 

70 """ 

71 return self.cashposition.sum() 

72 

73 @property 

74 def cashposition(self): 

75 """ 

76 The `cashposition` property computes the cash position of the portfolio, 

77 which is the amount of cash in the portfolio as a fraction of the total portfolio value. 

78 """ 

79 return self.prices * self.position 

80 

81 @property 

82 def position(self): 

83 """Get the current position. If the position is not yet set, then return an empty series.""" 

84 if self._position is None: 

85 return pd.Series(index=self.assets, dtype=float) 

86 

87 return self._position 

88 

89 @position.setter 

90 def position(self, position: np.array): 

91 """ 

92 Update the position of the state. Computes the required trades 

93 and updates other quantities (e.g. cash) accordingly. 

94 """ 

95 # update the position 

96 position = pd.Series(index=self.assets, data=position) 

97 

98 # compute the trades (can be fractional) 

99 self._trades = position.subtract(self.position, fill_value=0.0) 

100 

101 # update only now as otherwise the trades would be wrong 

102 self._position = position 

103 

104 @property 

105 def gmv(self): 

106 """ 

107 gross market value, e.g. abs(short) + long 

108 """ 

109 return self.cashposition.abs().sum() 

110 

111 @property 

112 def time(self): 

113 """ 

114 The time property returns the current time of the state. 

115 """ 

116 return self._time 

117 

118 @time.setter 

119 def time(self, time: datetime): 

120 """ 

121 Update the time of the state. Computes the number of days between 

122 the new and the previous time. 

123 """ 

124 if self.time is None: 

125 self._days = 0 

126 self._time = time 

127 else: 

128 self._days = (time - self.time).days 

129 self._time = time 

130 

131 @property 

132 def days(self): 

133 """Number of days between the current and the previous time. Most useful 

134 for computing the interest when holding cash.""" 

135 return self._days 

136 

137 @property 

138 def assets(self) -> pd.Index: 

139 """ 

140 The assets property returns the assets currently in the portfolio. 

141 """ 

142 if self._prices is None: 

143 return pd.Index(data=[], dtype=str) 

144 

145 return self.prices.dropna().index 

146 

147 @property 

148 def trades(self): 

149 """ 

150 The trades property returns the trades currently needed to reach the position. 

151 Most helpful when computing the trading costs following the move to 

152 a new position. 

153 """ 

154 return self._trades 

155 

156 @property 

157 def mask(self): 

158 """construct true/false mask for assets with missing prices""" 

159 if self._prices is None: 

160 return np.array([]) 

161 

162 return np.isfinite(self.prices.values) 

163 

164 @property 

165 def prices(self): 

166 """Get the current prices""" 

167 if self._prices is None: 

168 return pd.Series(dtype=float) 

169 return self._prices 

170 

171 @prices.setter 

172 def prices(self, prices): 

173 """ 

174 The prices property updates the prices of the assets in the portfolio. 

175 Also updates the aum of the portfolio by the profit achieved. 

176 """ 

177 value_before = (self.prices * self.position).sum() # self.cashposition.sum() 

178 value_after = (prices * self.position).sum() 

179 

180 self._prices = prices 

181 self._profit = value_after - value_before 

182 self.aum += self.profit 

183 

184 @property 

185 def profit(self): 

186 """ 

187 Following the price update the state updates the profit achieved 

188 between the previous and the current prices. 

189 """ 

190 return self._profit 

191 

192 @property 

193 def aum(self): 

194 """ 

195 The aum property returns the current assets under management (AUM) of the portfolio. 

196 """ 

197 return self._aum 

198 

199 @aum.setter 

200 def aum(self, aum): 

201 """ 

202 Setter for the aum property. It updates the aum property. 

203 """ 

204 self._aum = aum 

205 

206 @property 

207 def weights(self) -> pd.Series: 

208 """ 

209 The weights property computes the weighting of each asset in the current 

210 portfolio as a fraction of the total portfolio value (nav). 

211 

212 Returns: 

213 

214 a pandas series object containing the weighting of each asset as a 

215 fraction of the total portfolio value. If the positions are still 

216 missing, then a series of zeroes is returned. 

217 """ 

218 assert np.isclose(self.nav, self.aum), f"{self.nav} != {self.aum}" 

219 return self.cashposition / self.nav 

220 

221 @property 

222 def leverage(self) -> float: 

223 """ 

224 The `leverage` property computes the leverage of the portfolio, 

225 which is the sum of the absolute values of the portfolio weights. 

226 """ 

227 return float(self.weights.abs().sum())