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
« 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
17import numpy as np
18import pandas as pd
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 """
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
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
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
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
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()
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
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)
87 return self._position
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)
98 # compute the trades (can be fractional)
99 self._trades = position.subtract(self.position, fill_value=0.0)
101 # update only now as otherwise the trades would be wrong
102 self._position = position
104 @property
105 def gmv(self):
106 """
107 gross market value, e.g. abs(short) + long
108 """
109 return self.cashposition.abs().sum()
111 @property
112 def time(self):
113 """
114 The time property returns the current time of the state.
115 """
116 return self._time
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
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
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)
145 return self.prices.dropna().index
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
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([])
162 return np.isfinite(self.prices.values)
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
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()
180 self._prices = prices
181 self._profit = value_after - value_before
182 self.aum += self.profit
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
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
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
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).
212 Returns:
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
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())