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
« 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
16from dataclasses import dataclass
17from typing import Generator
19import numpy as np
20import pandas as pd
22from .portfolio import Portfolio
23from .state import State
24from .utils.interpolation import valid
25from .utils.rescale import returns2prices
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.
35 In each iteration we can update the portfolio by setting either
36 the weights, the position or the cash position.
38 After the iteration has been completed we build a Portfolio object
39 by calling the build method.
40 """
42 prices: pd.DataFrame
43 initial_aum: float = 1e6
45 _state: State = None
46 _units: pd.DataFrame = None
47 _aum: pd.Series = None
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.
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.
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 """
63 # assert isinstance(self.prices, pd.DataFrame)
64 assert self.prices.index.is_monotonic_increasing
65 assert self.prices.index.is_unique
67 self._state = State()
69 self._units = pd.DataFrame(
70 index=self.prices.index,
71 columns=self.prices.columns,
72 data=np.nan,
73 dtype=float,
74 )
76 self._aum = pd.Series(index=self.prices.index, dtype=float)
78 self._state.aum = self.initial_aum
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)
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()
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.
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)
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
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.
121 Yield:
123 time: a pandas DatetimeIndex object containing the dates seen so far.
124 state: the current state of the portfolio,
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]
132 # update the current time for the state
133 self._state.time = t
135 # yield the vector of times seen so far and the current state
136 yield self.index[self.index <= t], self._state
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.
144 Returns: pd.Series: a pandas Series object containing the current position of the portfolio.
145 """
146 return self._units.loc[self._state.time]
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.
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
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
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
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
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.
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.
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 """
194 return Portfolio(prices=self.prices, units=self.units, aum=self.aum)
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
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
211 @property
212 def aum(self):
213 """
214 The aum property returns the current AUM of the portfolio.
215 """
216 return self._aum
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
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)
231 prices = returns2prices(returns)
232 return cls(prices=prices)