src.cvxsimulator
1import importlib.metadata 2__version__ = importlib.metadata.version("cvxsimulator") 3 4from .builder import Builder 5from .portfolio import Portfolio 6from .state import State 7from .utils.interpolation import interpolate, valid 8 9__all__ = [ 10 "Builder", 11 "Portfolio", 12 "State", 13 "interpolate", 14 "valid" 15]
50@dataclass 51class Builder: 52 """The Builder is an auxiliary class used to build portfolios. 53 54 It overloads the __iter__ method to allow the class to iterate over 55 the timestamps for which the portfolio data is available. 56 57 In each iteration we can update the portfolio by setting either 58 the weights, the position or the cash position. 59 60 After the iteration has been completed we build a Portfolio object 61 by calling the build method. 62 """ 63 64 prices: pd.DataFrame 65 66 _state: State | None = None 67 _units: pd.DataFrame | None = None 68 _aum: pd.Series | None = None 69 initial_aum: float = 1e6 70 71 def __post_init__(self) -> None: 72 """Initialize the Builder instance after creation. 73 74 This method is automatically called after the object is initialized. 75 It sets up the internal state, creates empty DataFrames for units and AUM, 76 and initializes the AUM with the provided initial_aum value. 77 78 The method performs several validations on the prices DataFrame: 79 - Checks that the index is monotonically increasing 80 - Checks that the index has unique values 81 82 Returns 83 ------- 84 None 85 86 """ 87 # assert isinstance(self.prices, pd.DataFrame) 88 if not self.prices.index.is_monotonic_increasing: 89 raise ValueError("Index must be monotonically increasing") 90 91 if not self.prices.index.is_unique: 92 raise ValueError("Index must have unique values") 93 94 self._state = State() 95 96 self._units = pd.DataFrame( 97 index=self.prices.index, 98 columns=self.prices.columns, 99 data=np.nan, 100 dtype=float, 101 ) 102 103 self._aum = pd.Series(index=self.prices.index, dtype=float) 104 105 self._state.aum = self.initial_aum 106 107 @property 108 def valid(self): 109 """Check the validity of price data for each asset. 110 111 This property analyzes each column of the prices DataFrame to determine 112 if there are any missing values between the first and last valid data points. 113 114 Returns 115 ------- 116 pd.DataFrame 117 A DataFrame with the same columns as prices, containing boolean values 118 indicating whether each asset's price series is valid (True) or has 119 missing values in the middle (False) 120 121 Notes 122 ----- 123 A valid price series can have missing values at the beginning or end, 124 but not in the middle between the first and last valid data points. 125 126 """ 127 return self.prices.apply(valid) 128 129 @property 130 def intervals(self): 131 """Get the first and last valid index for each asset's price series. 132 133 This property identifies the time range for which each asset has valid price data. 134 135 Returns 136 ------- 137 pd.DataFrame 138 A DataFrame with assets as rows and two columns: 139 - 'first': The first valid index (timestamp) for each asset 140 - 'last': The last valid index (timestamp) for each asset 141 142 Notes 143 ----- 144 This is useful for determining the valid trading period for each asset, 145 especially when different assets have different data availability periods. 146 147 """ 148 return self.prices.apply( 149 lambda ts: pd.Series({"first": ts.first_valid_index(), "last": ts.last_valid_index()}) 150 ).transpose() 151 152 @property 153 def index(self) -> pd.DatetimeIndex: 154 """The index of the portfolio. 155 156 Returns: pd.Index: A pandas index representing the 157 time period for which the portfolio data is available. 158 """ 159 return pd.DatetimeIndex(self.prices.index) 160 161 @property 162 def current_prices(self) -> np.ndarray: 163 """Get the current prices for all assets in the portfolio. 164 165 This property retrieves the current prices from the internal state 166 for all assets that are currently in the portfolio. 167 168 Returns 169 ------- 170 np.array 171 An array of current prices for all assets in the portfolio 172 173 Notes 174 ----- 175 The prices are retrieved from the internal state, which is updated 176 during iteration through the portfolio's time index. 177 178 """ 179 return self._state.prices[self._state.assets].to_numpy() 180 181 def __iter__(self) -> Generator[tuple[pd.DatetimeIndex, State]]: 182 """Iterate over object in a for loop. 183 184 The method yields a list of dates seen so far and returns a tuple 185 containing the list of dates and the current portfolio state. 186 187 Yield: 188 time: a pandas DatetimeIndex object containing the dates seen so far. 189 state: the current state of the portfolio, 190 191 taking into account the stock prices at each interval. 192 193 """ 194 for t in self.index: 195 # update the current prices for the portfolio 196 self._state.prices = self.prices.loc[t] 197 198 # update the current time for the state 199 self._state.time = t 200 201 # yield the vector of times seen so far and the current state 202 yield self.index[self.index <= t], self._state 203 204 @property 205 def position(self) -> pd.Series: 206 """The position property returns the current position of the portfolio. 207 208 It returns a pandas Series object containing the current position of the portfolio. 209 210 Returns: pd.Series: a pandas Series object containing the current position of the portfolio. 211 """ 212 return self._units.loc[self._state.time] 213 214 @position.setter 215 def position(self, position: pd.Series) -> None: 216 """Set the current position of the portfolio. 217 218 This setter updates the position (number of units) for each asset in the portfolio 219 at the current time point. It also updates the internal state's position. 220 221 Parameters 222 ---------- 223 position : pd.Series 224 A pandas Series containing the new position (number of units) for each asset 225 226 Returns 227 ------- 228 None 229 230 """ 231 self._units.loc[self._state.time, self._state.assets] = position 232 self._state.position = position 233 234 @property 235 def cashposition(self): 236 """Get the current cash value of each position in the portfolio. 237 238 This property calculates the cash value of each position by multiplying 239 the number of units by the current price for each asset. 240 241 Returns 242 ------- 243 pd.Series 244 A pandas Series containing the cash value of each position, 245 indexed by asset 246 247 Notes 248 ----- 249 This is different from the 'cash' property, which represents 250 uninvested money. This property represents the market value 251 of each invested position. 252 253 """ 254 return self.position * self.current_prices 255 256 @property 257 def units(self): 258 """Get the complete history of portfolio holdings. 259 260 This property returns the entire DataFrame of holdings (units) for all 261 assets over all time points in the portfolio. 262 263 Returns 264 ------- 265 pd.DataFrame 266 A DataFrame containing the number of units held for each asset over time, 267 with dates as index and assets as columns 268 269 Notes 270 ----- 271 This property is particularly useful for testing and for building 272 the final Portfolio object via the build() method. 273 274 """ 275 return self._units 276 277 @cashposition.setter 278 def cashposition(self, cashposition: pd.Series) -> None: 279 """Set the current cash value of each position in the portfolio. 280 281 This setter updates the cash value of each position and automatically 282 converts the cash values to positions (units) using the current prices. 283 284 Parameters 285 ---------- 286 cashposition : pd.Series 287 A pandas Series containing the new cash value for each position, 288 indexed by asset 289 290 Returns 291 ------- 292 None 293 294 Notes 295 ----- 296 This is a convenient way to specify positions in terms of currency 297 amounts rather than number of units. The conversion formula is: 298 position = cashposition / prices 299 300 """ 301 self.position = cashposition / self.current_prices 302 303 def build(self): 304 """Create a new Portfolio instance from the current builder state. 305 306 This method creates a new immutable Portfolio object based on the 307 current state of the Builder, which can be used for analysis and reporting. 308 309 Returns 310 ------- 311 Portfolio 312 A new instance of the Portfolio class with the attributes 313 (prices, units, aum) as specified in the Builder 314 315 Notes 316 ----- 317 The resulting Portfolio object will be immutable (frozen) and will 318 have the same data as the Builder from which it was built, but 319 with a different interface focused on analysis rather than construction. 320 321 """ 322 return Portfolio(prices=self.prices, units=self.units, aum=self.aum) 323 324 @property 325 def weights(self) -> np.ndarray: 326 """Get the current portfolio weights for each asset. 327 328 This property retrieves the weight of each asset in the portfolio 329 from the internal state. Weights represent the proportion of the 330 portfolio's value invested in each asset. 331 332 Returns 333 ------- 334 np.array 335 An array of weights for each asset in the portfolio 336 337 Notes 338 ----- 339 Weights sum to 1.0 for a fully invested portfolio with no leverage. 340 Negative weights represent short positions. 341 342 """ 343 return self._state.weights[self._state.assets].to_numpy() 344 345 @weights.setter 346 def weights(self, weights: np.ndarray) -> None: 347 """Set the current portfolio weights for each asset. 348 349 This setter updates the portfolio weights and automatically converts 350 the weights to positions (units) using the current prices and NAV. 351 352 Parameters 353 ---------- 354 weights : np.array 355 An array of weights for each asset in the portfolio 356 357 Returns 358 ------- 359 None 360 361 Notes 362 ----- 363 This is a convenient way to rebalance the portfolio by specifying 364 the desired allocation as weights rather than exact positions. 365 The conversion formula is: position = NAV * weights / prices 366 367 """ 368 self.position = self._state.nav * weights / self.current_prices 369 370 @property 371 def aum(self): 372 """Get the assets under management (AUM) history of the portfolio. 373 374 This property returns the entire series of AUM values over time, 375 representing the total value of the portfolio at each time point. 376 377 Returns 378 ------- 379 pd.Series 380 A Series containing the AUM values over time, with dates as index 381 382 Notes 383 ----- 384 AUM (assets under management) represents the total value of the portfolio, 385 including both invested positions and uninvested cash. 386 387 """ 388 return self._aum 389 390 @aum.setter 391 def aum(self, aum): 392 """Set the current assets under management (AUM) of the portfolio. 393 394 This setter updates the AUM value at the current time point and 395 also updates the internal state's AUM. 396 397 Parameters 398 ---------- 399 aum : float 400 The new AUM value to set 401 402 Returns 403 ------- 404 None 405 406 Notes 407 ----- 408 Changing the AUM affects the portfolio's ability to take positions, 409 as position sizes are often calculated as a fraction of AUM. 410 411 """ 412 self._aum[self._state.time] = aum 413 self._state.aum = aum
The Builder is an auxiliary class used to build portfolios.
It overloads the __iter__ method to allow the class to iterate over the timestamps for which the portfolio data is available.
In each iteration we can update the portfolio by setting either the weights, the position or the cash position.
After the iteration has been completed we build a Portfolio object by calling the build method.
107 @property 108 def valid(self): 109 """Check the validity of price data for each asset. 110 111 This property analyzes each column of the prices DataFrame to determine 112 if there are any missing values between the first and last valid data points. 113 114 Returns 115 ------- 116 pd.DataFrame 117 A DataFrame with the same columns as prices, containing boolean values 118 indicating whether each asset's price series is valid (True) or has 119 missing values in the middle (False) 120 121 Notes 122 ----- 123 A valid price series can have missing values at the beginning or end, 124 but not in the middle between the first and last valid data points. 125 126 """ 127 return self.prices.apply(valid)
Check the validity of price data for each asset.
This property analyzes each column of the prices DataFrame to determine if there are any missing values between the first and last valid data points.
Returns
pd.DataFrame A DataFrame with the same columns as prices, containing boolean values indicating whether each asset's price series is valid (True) or has missing values in the middle (False)
Notes
A valid price series can have missing values at the beginning or end, but not in the middle between the first and last valid data points.
129 @property 130 def intervals(self): 131 """Get the first and last valid index for each asset's price series. 132 133 This property identifies the time range for which each asset has valid price data. 134 135 Returns 136 ------- 137 pd.DataFrame 138 A DataFrame with assets as rows and two columns: 139 - 'first': The first valid index (timestamp) for each asset 140 - 'last': The last valid index (timestamp) for each asset 141 142 Notes 143 ----- 144 This is useful for determining the valid trading period for each asset, 145 especially when different assets have different data availability periods. 146 147 """ 148 return self.prices.apply( 149 lambda ts: pd.Series({"first": ts.first_valid_index(), "last": ts.last_valid_index()}) 150 ).transpose()
Get the first and last valid index for each asset's price series.
This property identifies the time range for which each asset has valid price data.
Returns
pd.DataFrame A DataFrame with assets as rows and two columns: - 'first': The first valid index (timestamp) for each asset - 'last': The last valid index (timestamp) for each asset
Notes
This is useful for determining the valid trading period for each asset, especially when different assets have different data availability periods.
152 @property 153 def index(self) -> pd.DatetimeIndex: 154 """The index of the portfolio. 155 156 Returns: pd.Index: A pandas index representing the 157 time period for which the portfolio data is available. 158 """ 159 return pd.DatetimeIndex(self.prices.index)
The index of the portfolio.
Returns: pd.Index: A pandas index representing the time period for which the portfolio data is available.
161 @property 162 def current_prices(self) -> np.ndarray: 163 """Get the current prices for all assets in the portfolio. 164 165 This property retrieves the current prices from the internal state 166 for all assets that are currently in the portfolio. 167 168 Returns 169 ------- 170 np.array 171 An array of current prices for all assets in the portfolio 172 173 Notes 174 ----- 175 The prices are retrieved from the internal state, which is updated 176 during iteration through the portfolio's time index. 177 178 """ 179 return self._state.prices[self._state.assets].to_numpy()
Get the current prices for all assets in the portfolio.
This property retrieves the current prices from the internal state for all assets that are currently in the portfolio.
Returns
np.array An array of current prices for all assets in the portfolio
Notes
The prices are retrieved from the internal state, which is updated during iteration through the portfolio's time index.
204 @property 205 def position(self) -> pd.Series: 206 """The position property returns the current position of the portfolio. 207 208 It returns a pandas Series object containing the current position of the portfolio. 209 210 Returns: pd.Series: a pandas Series object containing the current position of the portfolio. 211 """ 212 return self._units.loc[self._state.time]
The position property returns the current position of the portfolio.
It returns a pandas Series object containing the current position of the portfolio.
Returns: pd.Series: a pandas Series object containing the current position of the portfolio.
234 @property 235 def cashposition(self): 236 """Get the current cash value of each position in the portfolio. 237 238 This property calculates the cash value of each position by multiplying 239 the number of units by the current price for each asset. 240 241 Returns 242 ------- 243 pd.Series 244 A pandas Series containing the cash value of each position, 245 indexed by asset 246 247 Notes 248 ----- 249 This is different from the 'cash' property, which represents 250 uninvested money. This property represents the market value 251 of each invested position. 252 253 """ 254 return self.position * self.current_prices
Get the current cash value of each position in the portfolio.
This property calculates the cash value of each position by multiplying the number of units by the current price for each asset.
Returns
pd.Series A pandas Series containing the cash value of each position, indexed by asset
Notes
This is different from the 'cash' property, which represents uninvested money. This property represents the market value of each invested position.
256 @property 257 def units(self): 258 """Get the complete history of portfolio holdings. 259 260 This property returns the entire DataFrame of holdings (units) for all 261 assets over all time points in the portfolio. 262 263 Returns 264 ------- 265 pd.DataFrame 266 A DataFrame containing the number of units held for each asset over time, 267 with dates as index and assets as columns 268 269 Notes 270 ----- 271 This property is particularly useful for testing and for building 272 the final Portfolio object via the build() method. 273 274 """ 275 return self._units
Get the complete history of portfolio holdings.
This property returns the entire DataFrame of holdings (units) for all assets over all time points in the portfolio.
Returns
pd.DataFrame A DataFrame containing the number of units held for each asset over time, with dates as index and assets as columns
Notes
This property is particularly useful for testing and for building the final Portfolio object via the build() method.
303 def build(self): 304 """Create a new Portfolio instance from the current builder state. 305 306 This method creates a new immutable Portfolio object based on the 307 current state of the Builder, which can be used for analysis and reporting. 308 309 Returns 310 ------- 311 Portfolio 312 A new instance of the Portfolio class with the attributes 313 (prices, units, aum) as specified in the Builder 314 315 Notes 316 ----- 317 The resulting Portfolio object will be immutable (frozen) and will 318 have the same data as the Builder from which it was built, but 319 with a different interface focused on analysis rather than construction. 320 321 """ 322 return Portfolio(prices=self.prices, units=self.units, aum=self.aum)
Create a new Portfolio instance from the current builder state.
This method creates a new immutable Portfolio object based on the current state of the Builder, which can be used for analysis and reporting.
Returns
Portfolio A new instance of the Portfolio class with the attributes (prices, units, aum) as specified in the Builder
Notes
The resulting Portfolio object will be immutable (frozen) and will have the same data as the Builder from which it was built, but with a different interface focused on analysis rather than construction.
324 @property 325 def weights(self) -> np.ndarray: 326 """Get the current portfolio weights for each asset. 327 328 This property retrieves the weight of each asset in the portfolio 329 from the internal state. Weights represent the proportion of the 330 portfolio's value invested in each asset. 331 332 Returns 333 ------- 334 np.array 335 An array of weights for each asset in the portfolio 336 337 Notes 338 ----- 339 Weights sum to 1.0 for a fully invested portfolio with no leverage. 340 Negative weights represent short positions. 341 342 """ 343 return self._state.weights[self._state.assets].to_numpy()
Get the current portfolio weights for each asset.
This property retrieves the weight of each asset in the portfolio from the internal state. Weights represent the proportion of the portfolio's value invested in each asset.
Returns
np.array An array of weights for each asset in the portfolio
Notes
Weights sum to 1.0 for a fully invested portfolio with no leverage. Negative weights represent short positions.
370 @property 371 def aum(self): 372 """Get the assets under management (AUM) history of the portfolio. 373 374 This property returns the entire series of AUM values over time, 375 representing the total value of the portfolio at each time point. 376 377 Returns 378 ------- 379 pd.Series 380 A Series containing the AUM values over time, with dates as index 381 382 Notes 383 ----- 384 AUM (assets under management) represents the total value of the portfolio, 385 including both invested positions and uninvested cash. 386 387 """ 388 return self._aum
Get the assets under management (AUM) history of the portfolio.
This property returns the entire series of AUM values over time, representing the total value of the portfolio at each time point.
Returns
pd.Series A Series containing the AUM values over time, with dates as index
Notes
AUM (assets under management) represents the total value of the portfolio, including both invested positions and uninvested cash.
33@dataclass(frozen=True) 34class Portfolio: 35 """Represents a portfolio of assets with methods for analysis and visualization. 36 37 The Portfolio class is a frozen dataclass (immutable) that represents a portfolio 38 of assets with their prices and positions (units). It provides methods for 39 calculating various metrics like NAV, profit, drawdown, and for visualizing 40 the portfolio's performance. 41 42 Attributes 43 ---------- 44 prices : pd.DataFrame 45 DataFrame of asset prices over time, with dates as index and assets as columns 46 units : pd.DataFrame 47 DataFrame of asset positions (units) over time, with dates as index and assets as columns 48 aum : Union[float, pd.Series] 49 Assets under management, either as a constant float or as a Series over time 50 51 """ 52 53 prices: pd.DataFrame 54 units: pd.DataFrame 55 aum: float | pd.Series 56 _data: Data = field(init=False) 57 58 def __post_init__(self) -> None: 59 """Validate the portfolio data after initialization. 60 61 This method is automatically called after an instance of the Portfolio 62 class has been initialized. It performs a series of validation checks 63 to ensure that the prices and units dataframes are in the expected format 64 with no duplicates or missing data. 65 66 The method checks that: 67 - Both prices and units dataframes have monotonic increasing indices 68 - Both prices and units dataframes have unique indices 69 - The index of units is a subset of the index of prices 70 - The columns of units is a subset of the columns of prices 71 72 Raises 73 ------ 74 AssertionError 75 If any of the validation checks fail 76 77 """ 78 if not self.prices.index.is_monotonic_increasing: 79 raise ValueError("`prices` index must be monotonic increasing.") 80 81 if not self.prices.index.is_unique: 82 raise ValueError("`prices` index must be unique.") 83 84 if not self.units.index.is_monotonic_increasing: 85 raise ValueError("`units` index must be monotonic increasing.") 86 87 if not self.units.index.is_unique: 88 raise ValueError("`units` index must be unique.") 89 90 missing_dates = self.units.index.difference(self.prices.index) 91 if not missing_dates.empty: 92 raise ValueError(f"`units` index contains dates not present in `prices`: {missing_dates.tolist()}") 93 94 missing_assets = self.units.columns.difference(self.prices.columns) 95 if not missing_assets.empty: 96 raise ValueError(f"`units` contains assets not present in `prices`: {missing_assets.tolist()}") 97 98 frame = self.nav.pct_change().to_frame() 99 frame.index.name = "Date" 100 d = build_data(returns=frame) 101 102 object.__setattr__(self, "_data", d) 103 104 @property 105 def index(self) -> list[datetime]: 106 """Get the time index of the portfolio. 107 108 Returns 109 ------- 110 pd.DatetimeIndex 111 A DatetimeIndex representing the time period for which portfolio 112 data is available 113 114 Notes 115 ----- 116 This property extracts the index from the prices DataFrame, which 117 represents all time points in the portfolio history. 118 119 """ 120 return pd.DatetimeIndex(self.prices.index).to_list() 121 122 @property 123 def assets(self) -> list[str]: 124 """Get the list of assets in the portfolio. 125 126 Returns 127 ------- 128 pd.Index 129 An Index containing the names of all assets in the portfolio 130 131 Notes 132 ----- 133 This property extracts the column names from the prices DataFrame, 134 which correspond to all assets for which price data is available. 135 136 """ 137 return self.prices.columns.to_list() 138 139 @property 140 def nav(self) -> pd.Series: 141 """Get the net asset value (NAV) of the portfolio over time. 142 143 The NAV represents the total value of the portfolio at each point in time. 144 If aum is provided as a Series, it is used directly. Otherwise, the NAV 145 is calculated from the cumulative profit plus the initial aum. 146 147 Returns 148 ------- 149 pd.Series 150 Series representing the NAV of the portfolio over time 151 152 """ 153 if isinstance(self.aum, pd.Series): 154 series = self.aum 155 else: 156 profit = (self.cashposition.shift(1) * self.returns.fillna(0.0)).sum(axis=1) 157 series = profit.cumsum() + self.aum 158 159 series.name = "NAV" 160 return series 161 162 @property 163 def profit(self) -> pd.Series: 164 """Get the profit/loss of the portfolio at each time point. 165 166 This calculates the profit or loss at each time point based on the 167 previous positions and the returns of each asset. 168 169 Returns 170 ------- 171 pd.Series 172 Series representing the profit/loss at each time point 173 174 Notes 175 ----- 176 The profit is calculated by multiplying the previous day's positions 177 (in currency terms) by the returns of each asset, and then summing 178 across all assets. 179 180 """ 181 series = (self.cashposition.shift(1) * self.returns.fillna(0.0)).sum(axis=1) 182 series.name = "Profit" 183 return series 184 185 @property 186 def cashposition(self) -> pd.DataFrame: 187 """Get the cash value of each position over time. 188 189 This calculates the cash value of each position by multiplying 190 the number of units by the price for each asset at each time point. 191 192 Returns 193 ------- 194 pd.DataFrame 195 DataFrame with the cash value of each position over time, 196 with dates as index and assets as columns 197 198 """ 199 return self.prices * self.units 200 201 @property 202 def returns(self) -> pd.DataFrame: 203 """Get the returns of individual assets over time. 204 205 This calculates the percentage change in price for each asset 206 from one time point to the next. 207 208 Returns 209 ------- 210 pd.DataFrame 211 DataFrame with the returns of each asset over time, 212 with dates as index and assets as columns 213 214 """ 215 return self.prices.pct_change() 216 217 @property 218 def trades_units(self) -> pd.DataFrame: 219 """Get the trades made in the portfolio in terms of units. 220 221 This calculates the changes in position (units) from one time point 222 to the next for each asset. 223 224 Returns 225 ------- 226 pd.DataFrame 227 DataFrame with the trades (changes in units) for each asset over time, 228 with dates as index and assets as columns 229 230 Notes 231 ----- 232 Calculated as the difference between consecutive position values. 233 Positive values represent buys, negative values represent sells. 234 The first row contains the initial positions, as there are no previous 235 positions to compare with. 236 237 """ 238 t = self.units.fillna(0.0).diff() 239 t.loc[self.index[0]] = self.units.loc[self.index[0]] 240 return t.fillna(0.0) 241 242 @property 243 def trades_currency(self) -> pd.DataFrame: 244 """Get the trades made in the portfolio in terms of currency. 245 246 This calculates the cash value of trades by multiplying the changes 247 in position (units) by the current prices. 248 249 Returns 250 ------- 251 pd.DataFrame 252 DataFrame with the cash value of trades for each asset over time, 253 with dates as index and assets as columns 254 255 Notes 256 ----- 257 Calculated by multiplying trades_units by prices. 258 Positive values represent buys (cash outflows), 259 negative values represent sells (cash inflows). 260 261 """ 262 return self.trades_units * self.prices 263 264 @property 265 def turnover_relative(self) -> pd.DataFrame: 266 """Get the turnover relative to the portfolio NAV. 267 268 This calculates the trades as a percentage of the portfolio NAV, 269 which provides a measure of trading activity relative to portfolio size. 270 271 Returns 272 ------- 273 pd.DataFrame 274 DataFrame with the relative turnover for each asset over time, 275 with dates as index and assets as columns 276 277 Notes 278 ----- 279 Calculated by dividing trades_currency by NAV. 280 Positive values represent buys, negative values represent sells. 281 A value of 0.05 means a buy equal to 5% of the portfolio NAV. 282 283 """ 284 return self.trades_currency.div(self.nav, axis=0) 285 286 @property 287 def turnover(self) -> pd.DataFrame: 288 """Get the absolute turnover in the portfolio. 289 290 This calculates the absolute value of trades in currency terms, 291 which provides a measure of total trading activity regardless of 292 direction (buy or sell). 293 294 Returns 295 ------- 296 pd.DataFrame 297 DataFrame with the absolute turnover for each asset over time, 298 with dates as index and assets as columns 299 300 Notes 301 ----- 302 Calculated as the absolute value of trades_currency. 303 This is useful for calculating trading costs that apply equally 304 to buys and sells. 305 306 """ 307 return self.trades_currency.abs() 308 309 def __getitem__(self, time: datetime | str | pd.Timestamp) -> pd.Series: 310 """Get the portfolio positions (units) at a specific time. 311 312 This method allows for dictionary-like access to the portfolio positions 313 at a specific time point using the syntax: portfolio[time]. 314 315 Parameters 316 ---------- 317 time : Union[datetime, str, pd.Timestamp] 318 The time index for which to retrieve the positions 319 320 Returns 321 ------- 322 pd.Series 323 Series containing the positions (units) for each asset at the specified time 324 325 Raises 326 ------ 327 KeyError 328 If the specified time is not in the portfolio's index 329 330 Examples 331 -------- 332 ``` 333 portfolio['2023-01-01'] # Get positions on January 1, 2023 334 portfolio[pd.Timestamp('2023-01-01')] # Same as above 335 ``` 336 337 """ 338 return self.units.loc[time] 339 340 @property 341 def equity(self) -> pd.DataFrame: 342 """Get the equity (cash value) of each position over time. 343 344 This property returns the cash value of each position in the portfolio, 345 calculated by multiplying the number of units by the price for each asset. 346 347 Returns 348 ------- 349 pd.DataFrame 350 DataFrame with the cash value of each position over time, 351 with dates as index and assets as columns 352 353 Notes 354 ----- 355 This is an alias for the cashposition property and returns the same values. 356 The term "equity" is used in the context of the cash value of positions, 357 not to be confused with the equity asset class. 358 359 """ 360 return self.cashposition 361 362 @property 363 def weights(self) -> pd.DataFrame: 364 """Get the weight of each asset in the portfolio over time. 365 366 This calculates the relative weight of each asset in the portfolio 367 by dividing the cash value of each position by the total portfolio 368 value (NAV) at each time point. 369 370 Returns 371 ------- 372 pd.DataFrame 373 DataFrame with the weight of each asset over time, 374 with dates as index and assets as columns 375 376 Notes 377 ----- 378 The sum of weights across all assets at any given time should equal 1.0 379 for a fully invested portfolio with no leverage. Weights can be negative 380 for short positions. 381 382 """ 383 return self.equity.apply(lambda x: x / self.nav) 384 385 @property 386 def stats(self): 387 """Get statistical analysis data for the portfolio. 388 389 This property provides access to various statistical metrics calculated 390 for the portfolio, such as Sharpe ratio, volatility, drawdowns, etc. 391 392 Returns 393 ------- 394 object 395 An object containing various statistical metrics for the portfolio 396 397 Notes 398 ----- 399 The statistics are calculated by the underlying jquantstats library 400 and are based on the portfolio's NAV time series. 401 402 """ 403 return self._data.stats 404 405 @property 406 def plots(self): 407 """Get visualization tools for the portfolio. 408 409 This property provides access to various plotting functions for visualizing 410 the portfolio's performance, returns, drawdowns, etc. 411 412 Returns 413 ------- 414 object 415 An object containing various plotting methods for the portfolio 416 417 Notes 418 ----- 419 The plotting functions are provided by the underlying jquantstats library 420 and operate on the portfolio's NAV time series. 421 422 """ 423 return self._data.plots 424 425 @property 426 def reports(self): 427 """Get reporting tools for the portfolio. 428 429 This property provides access to various reporting functions for generating 430 performance reports, risk metrics, and other analytics for the portfolio. 431 432 Returns 433 ------- 434 object 435 An object containing various reporting methods for the portfolio 436 437 Notes 438 ----- 439 The reporting functions are provided by the underlying jquantstats library 440 and operate on the portfolio's NAV time series. 441 442 """ 443 return self._data.reports 444 445 def sharpe(self, periods=None): 446 """Calculate the Sharpe ratio for the portfolio. 447 448 The Sharpe ratio is a measure of risk-adjusted return, calculated as 449 the portfolio's excess return divided by its volatility. 450 451 Parameters 452 ---------- 453 periods : int, optional 454 The number of periods per year for annualization. 455 For daily data, use 252; for weekly data, use 52; for monthly data, use 12. 456 If None, no annualization is performed. 457 458 Returns 459 ------- 460 float 461 The Sharpe ratio of the portfolio 462 463 Notes 464 ----- 465 The Sharpe ratio is calculated using the portfolio's NAV time series. 466 A higher Sharpe ratio indicates better risk-adjusted performance. 467 468 """ 469 return self.stats.sharpe(periods=periods)["NAV"] 470 471 @classmethod 472 def from_cashpos_prices(cls, prices: pd.DataFrame, cashposition: pd.DataFrame, aum: float): 473 """Create a Portfolio instance from cash positions and prices. 474 475 This class method provides an alternative way to create a Portfolio instance 476 when you have the cash positions rather than the number of units. 477 478 Parameters 479 ---------- 480 prices : pd.DataFrame 481 DataFrame of asset prices over time, with dates as index and assets as columns 482 cashposition : pd.DataFrame 483 DataFrame of cash positions over time, with dates as index and assets as columns 484 aum : float 485 Assets under management 486 487 Returns 488 ------- 489 Portfolio 490 A new Portfolio instance with units calculated from cash positions and prices 491 492 Notes 493 ----- 494 The units are calculated by dividing the cash positions by the prices. 495 This is useful when you have the monetary value of each position rather 496 than the number of units. 497 498 """ 499 units = cashposition.div(prices, fill_value=0.0) 500 return cls(prices=prices, units=units, aum=aum) 501 502 def snapshot(self, title: str = "Portfolio Summary", log_scale: bool = True): 503 """Generate and display a snapshot of the portfolio summary. 504 505 This method creates a visual representation of the portfolio summary 506 using the associated plot functionalities. The snapshot can be 507 configured with a title and whether to use a logarithmic scale. 508 509 Args: 510 title: A string specifying the title of the snapshot. 511 Default is "Portfolio Summary". 512 log_scale: A boolean indicating whether to display the plot 513 using a logarithmic scale. Default is True. 514 515 Returns: 516 The generated plot object representing the portfolio snapshot. 517 518 """ 519 return self.plots.plot_snapshot(title=title, log_scale=log_scale)
Represents a portfolio of assets with methods for analysis and visualization.
The Portfolio class is a frozen dataclass (immutable) that represents a portfolio of assets with their prices and positions (units). It provides methods for calculating various metrics like NAV, profit, drawdown, and for visualizing the portfolio's performance.
Attributes
prices : pd.DataFrame DataFrame of asset prices over time, with dates as index and assets as columns units : pd.DataFrame DataFrame of asset positions (units) over time, with dates as index and assets as columns aum : Union[float, pd.Series] Assets under management, either as a constant float or as a Series over time
104 @property 105 def index(self) -> list[datetime]: 106 """Get the time index of the portfolio. 107 108 Returns 109 ------- 110 pd.DatetimeIndex 111 A DatetimeIndex representing the time period for which portfolio 112 data is available 113 114 Notes 115 ----- 116 This property extracts the index from the prices DataFrame, which 117 represents all time points in the portfolio history. 118 119 """ 120 return pd.DatetimeIndex(self.prices.index).to_list()
Get the time index of the portfolio.
Returns
pd.DatetimeIndex A DatetimeIndex representing the time period for which portfolio data is available
Notes
This property extracts the index from the prices DataFrame, which represents all time points in the portfolio history.
122 @property 123 def assets(self) -> list[str]: 124 """Get the list of assets in the portfolio. 125 126 Returns 127 ------- 128 pd.Index 129 An Index containing the names of all assets in the portfolio 130 131 Notes 132 ----- 133 This property extracts the column names from the prices DataFrame, 134 which correspond to all assets for which price data is available. 135 136 """ 137 return self.prices.columns.to_list()
Get the list of assets in the portfolio.
Returns
pd.Index An Index containing the names of all assets in the portfolio
Notes
This property extracts the column names from the prices DataFrame, which correspond to all assets for which price data is available.
162 @property 163 def profit(self) -> pd.Series: 164 """Get the profit/loss of the portfolio at each time point. 165 166 This calculates the profit or loss at each time point based on the 167 previous positions and the returns of each asset. 168 169 Returns 170 ------- 171 pd.Series 172 Series representing the profit/loss at each time point 173 174 Notes 175 ----- 176 The profit is calculated by multiplying the previous day's positions 177 (in currency terms) by the returns of each asset, and then summing 178 across all assets. 179 180 """ 181 series = (self.cashposition.shift(1) * self.returns.fillna(0.0)).sum(axis=1) 182 series.name = "Profit" 183 return series
Get the profit/loss of the portfolio at each time point.
This calculates the profit or loss at each time point based on the previous positions and the returns of each asset.
Returns
pd.Series Series representing the profit/loss at each time point
Notes
The profit is calculated by multiplying the previous day's positions (in currency terms) by the returns of each asset, and then summing across all assets.
185 @property 186 def cashposition(self) -> pd.DataFrame: 187 """Get the cash value of each position over time. 188 189 This calculates the cash value of each position by multiplying 190 the number of units by the price for each asset at each time point. 191 192 Returns 193 ------- 194 pd.DataFrame 195 DataFrame with the cash value of each position over time, 196 with dates as index and assets as columns 197 198 """ 199 return self.prices * self.units
Get the cash value of each position over time.
This calculates the cash value of each position by multiplying the number of units by the price for each asset at each time point.
Returns
pd.DataFrame DataFrame with the cash value of each position over time, with dates as index and assets as columns
201 @property 202 def returns(self) -> pd.DataFrame: 203 """Get the returns of individual assets over time. 204 205 This calculates the percentage change in price for each asset 206 from one time point to the next. 207 208 Returns 209 ------- 210 pd.DataFrame 211 DataFrame with the returns of each asset over time, 212 with dates as index and assets as columns 213 214 """ 215 return self.prices.pct_change()
Get the returns of individual assets over time.
This calculates the percentage change in price for each asset from one time point to the next.
Returns
pd.DataFrame DataFrame with the returns of each asset over time, with dates as index and assets as columns
217 @property 218 def trades_units(self) -> pd.DataFrame: 219 """Get the trades made in the portfolio in terms of units. 220 221 This calculates the changes in position (units) from one time point 222 to the next for each asset. 223 224 Returns 225 ------- 226 pd.DataFrame 227 DataFrame with the trades (changes in units) for each asset over time, 228 with dates as index and assets as columns 229 230 Notes 231 ----- 232 Calculated as the difference between consecutive position values. 233 Positive values represent buys, negative values represent sells. 234 The first row contains the initial positions, as there are no previous 235 positions to compare with. 236 237 """ 238 t = self.units.fillna(0.0).diff() 239 t.loc[self.index[0]] = self.units.loc[self.index[0]] 240 return t.fillna(0.0)
Get the trades made in the portfolio in terms of units.
This calculates the changes in position (units) from one time point to the next for each asset.
Returns
pd.DataFrame DataFrame with the trades (changes in units) for each asset over time, with dates as index and assets as columns
Notes
Calculated as the difference between consecutive position values. Positive values represent buys, negative values represent sells. The first row contains the initial positions, as there are no previous positions to compare with.
242 @property 243 def trades_currency(self) -> pd.DataFrame: 244 """Get the trades made in the portfolio in terms of currency. 245 246 This calculates the cash value of trades by multiplying the changes 247 in position (units) by the current prices. 248 249 Returns 250 ------- 251 pd.DataFrame 252 DataFrame with the cash value of trades for each asset over time, 253 with dates as index and assets as columns 254 255 Notes 256 ----- 257 Calculated by multiplying trades_units by prices. 258 Positive values represent buys (cash outflows), 259 negative values represent sells (cash inflows). 260 261 """ 262 return self.trades_units * self.prices
Get the trades made in the portfolio in terms of currency.
This calculates the cash value of trades by multiplying the changes in position (units) by the current prices.
Returns
pd.DataFrame DataFrame with the cash value of trades for each asset over time, with dates as index and assets as columns
Notes
Calculated by multiplying trades_units by prices. Positive values represent buys (cash outflows), negative values represent sells (cash inflows).
264 @property 265 def turnover_relative(self) -> pd.DataFrame: 266 """Get the turnover relative to the portfolio NAV. 267 268 This calculates the trades as a percentage of the portfolio NAV, 269 which provides a measure of trading activity relative to portfolio size. 270 271 Returns 272 ------- 273 pd.DataFrame 274 DataFrame with the relative turnover for each asset over time, 275 with dates as index and assets as columns 276 277 Notes 278 ----- 279 Calculated by dividing trades_currency by NAV. 280 Positive values represent buys, negative values represent sells. 281 A value of 0.05 means a buy equal to 5% of the portfolio NAV. 282 283 """ 284 return self.trades_currency.div(self.nav, axis=0)
Get the turnover relative to the portfolio NAV.
This calculates the trades as a percentage of the portfolio NAV, which provides a measure of trading activity relative to portfolio size.
Returns
pd.DataFrame DataFrame with the relative turnover for each asset over time, with dates as index and assets as columns
Notes
Calculated by dividing trades_currency by NAV. Positive values represent buys, negative values represent sells. A value of 0.05 means a buy equal to 5% of the portfolio NAV.
286 @property 287 def turnover(self) -> pd.DataFrame: 288 """Get the absolute turnover in the portfolio. 289 290 This calculates the absolute value of trades in currency terms, 291 which provides a measure of total trading activity regardless of 292 direction (buy or sell). 293 294 Returns 295 ------- 296 pd.DataFrame 297 DataFrame with the absolute turnover for each asset over time, 298 with dates as index and assets as columns 299 300 Notes 301 ----- 302 Calculated as the absolute value of trades_currency. 303 This is useful for calculating trading costs that apply equally 304 to buys and sells. 305 306 """ 307 return self.trades_currency.abs()
Get the absolute turnover in the portfolio.
This calculates the absolute value of trades in currency terms, which provides a measure of total trading activity regardless of direction (buy or sell).
Returns
pd.DataFrame DataFrame with the absolute turnover for each asset over time, with dates as index and assets as columns
Notes
Calculated as the absolute value of trades_currency. This is useful for calculating trading costs that apply equally to buys and sells.
340 @property 341 def equity(self) -> pd.DataFrame: 342 """Get the equity (cash value) of each position over time. 343 344 This property returns the cash value of each position in the portfolio, 345 calculated by multiplying the number of units by the price for each asset. 346 347 Returns 348 ------- 349 pd.DataFrame 350 DataFrame with the cash value of each position over time, 351 with dates as index and assets as columns 352 353 Notes 354 ----- 355 This is an alias for the cashposition property and returns the same values. 356 The term "equity" is used in the context of the cash value of positions, 357 not to be confused with the equity asset class. 358 359 """ 360 return self.cashposition
Get the equity (cash value) of each position over time.
This property returns the cash value of each position in the portfolio, calculated by multiplying the number of units by the price for each asset.
Returns
pd.DataFrame DataFrame with the cash value of each position over time, with dates as index and assets as columns
Notes
This is an alias for the cashposition property and returns the same values. The term "equity" is used in the context of the cash value of positions, not to be confused with the equity asset class.
362 @property 363 def weights(self) -> pd.DataFrame: 364 """Get the weight of each asset in the portfolio over time. 365 366 This calculates the relative weight of each asset in the portfolio 367 by dividing the cash value of each position by the total portfolio 368 value (NAV) at each time point. 369 370 Returns 371 ------- 372 pd.DataFrame 373 DataFrame with the weight of each asset over time, 374 with dates as index and assets as columns 375 376 Notes 377 ----- 378 The sum of weights across all assets at any given time should equal 1.0 379 for a fully invested portfolio with no leverage. Weights can be negative 380 for short positions. 381 382 """ 383 return self.equity.apply(lambda x: x / self.nav)
Get the weight of each asset in the portfolio over time.
This calculates the relative weight of each asset in the portfolio by dividing the cash value of each position by the total portfolio value (NAV) at each time point.
Returns
pd.DataFrame DataFrame with the weight of each asset over time, with dates as index and assets as columns
Notes
The sum of weights across all assets at any given time should equal 1.0 for a fully invested portfolio with no leverage. Weights can be negative for short positions.
385 @property 386 def stats(self): 387 """Get statistical analysis data for the portfolio. 388 389 This property provides access to various statistical metrics calculated 390 for the portfolio, such as Sharpe ratio, volatility, drawdowns, etc. 391 392 Returns 393 ------- 394 object 395 An object containing various statistical metrics for the portfolio 396 397 Notes 398 ----- 399 The statistics are calculated by the underlying jquantstats library 400 and are based on the portfolio's NAV time series. 401 402 """ 403 return self._data.stats
Get statistical analysis data for the portfolio.
This property provides access to various statistical metrics calculated for the portfolio, such as Sharpe ratio, volatility, drawdowns, etc.
Returns
object An object containing various statistical metrics for the portfolio
Notes
The statistics are calculated by the underlying jquantstats library and are based on the portfolio's NAV time series.
405 @property 406 def plots(self): 407 """Get visualization tools for the portfolio. 408 409 This property provides access to various plotting functions for visualizing 410 the portfolio's performance, returns, drawdowns, etc. 411 412 Returns 413 ------- 414 object 415 An object containing various plotting methods for the portfolio 416 417 Notes 418 ----- 419 The plotting functions are provided by the underlying jquantstats library 420 and operate on the portfolio's NAV time series. 421 422 """ 423 return self._data.plots
Get visualization tools for the portfolio.
This property provides access to various plotting functions for visualizing the portfolio's performance, returns, drawdowns, etc.
Returns
object An object containing various plotting methods for the portfolio
Notes
The plotting functions are provided by the underlying jquantstats library and operate on the portfolio's NAV time series.
425 @property 426 def reports(self): 427 """Get reporting tools for the portfolio. 428 429 This property provides access to various reporting functions for generating 430 performance reports, risk metrics, and other analytics for the portfolio. 431 432 Returns 433 ------- 434 object 435 An object containing various reporting methods for the portfolio 436 437 Notes 438 ----- 439 The reporting functions are provided by the underlying jquantstats library 440 and operate on the portfolio's NAV time series. 441 442 """ 443 return self._data.reports
Get reporting tools for the portfolio.
This property provides access to various reporting functions for generating performance reports, risk metrics, and other analytics for the portfolio.
Returns
object An object containing various reporting methods for the portfolio
Notes
The reporting functions are provided by the underlying jquantstats library and operate on the portfolio's NAV time series.
445 def sharpe(self, periods=None): 446 """Calculate the Sharpe ratio for the portfolio. 447 448 The Sharpe ratio is a measure of risk-adjusted return, calculated as 449 the portfolio's excess return divided by its volatility. 450 451 Parameters 452 ---------- 453 periods : int, optional 454 The number of periods per year for annualization. 455 For daily data, use 252; for weekly data, use 52; for monthly data, use 12. 456 If None, no annualization is performed. 457 458 Returns 459 ------- 460 float 461 The Sharpe ratio of the portfolio 462 463 Notes 464 ----- 465 The Sharpe ratio is calculated using the portfolio's NAV time series. 466 A higher Sharpe ratio indicates better risk-adjusted performance. 467 468 """ 469 return self.stats.sharpe(periods=periods)["NAV"]
Calculate the Sharpe ratio for the portfolio.
The Sharpe ratio is a measure of risk-adjusted return, calculated as the portfolio's excess return divided by its volatility.
Parameters
periods : int, optional The number of periods per year for annualization. For daily data, use 252; for weekly data, use 52; for monthly data, use 12. If None, no annualization is performed.
Returns
float The Sharpe ratio of the portfolio
Notes
The Sharpe ratio is calculated using the portfolio's NAV time series. A higher Sharpe ratio indicates better risk-adjusted performance.
471 @classmethod 472 def from_cashpos_prices(cls, prices: pd.DataFrame, cashposition: pd.DataFrame, aum: float): 473 """Create a Portfolio instance from cash positions and prices. 474 475 This class method provides an alternative way to create a Portfolio instance 476 when you have the cash positions rather than the number of units. 477 478 Parameters 479 ---------- 480 prices : pd.DataFrame 481 DataFrame of asset prices over time, with dates as index and assets as columns 482 cashposition : pd.DataFrame 483 DataFrame of cash positions over time, with dates as index and assets as columns 484 aum : float 485 Assets under management 486 487 Returns 488 ------- 489 Portfolio 490 A new Portfolio instance with units calculated from cash positions and prices 491 492 Notes 493 ----- 494 The units are calculated by dividing the cash positions by the prices. 495 This is useful when you have the monetary value of each position rather 496 than the number of units. 497 498 """ 499 units = cashposition.div(prices, fill_value=0.0) 500 return cls(prices=prices, units=units, aum=aum)
Create a Portfolio instance from cash positions and prices.
This class method provides an alternative way to create a Portfolio instance when you have the cash positions rather than the number of units.
Parameters
prices : pd.DataFrame DataFrame of asset prices over time, with dates as index and assets as columns cashposition : pd.DataFrame DataFrame of cash positions over time, with dates as index and assets as columns aum : float Assets under management
Returns
Portfolio A new Portfolio instance with units calculated from cash positions and prices
Notes
The units are calculated by dividing the cash positions by the prices. This is useful when you have the monetary value of each position rather than the number of units.
502 def snapshot(self, title: str = "Portfolio Summary", log_scale: bool = True): 503 """Generate and display a snapshot of the portfolio summary. 504 505 This method creates a visual representation of the portfolio summary 506 using the associated plot functionalities. The snapshot can be 507 configured with a title and whether to use a logarithmic scale. 508 509 Args: 510 title: A string specifying the title of the snapshot. 511 Default is "Portfolio Summary". 512 log_scale: A boolean indicating whether to display the plot 513 using a logarithmic scale. Default is True. 514 515 Returns: 516 The generated plot object representing the portfolio snapshot. 517 518 """ 519 return self.plots.plot_snapshot(title=title, log_scale=log_scale)
Generate and display a snapshot of the portfolio summary.
This method creates a visual representation of the portfolio summary using the associated plot functionalities. The snapshot can be configured with a title and whether to use a logarithmic scale.
Args: title: A string specifying the title of the snapshot. Default is "Portfolio Summary". log_scale: A boolean indicating whether to display the plot using a logarithmic scale. Default is True.
Returns: The generated plot object representing the portfolio snapshot.
29@dataclass() 30class State: 31 """Represents the current state of a portfolio during simulation. 32 33 The State class tracks the current positions, prices, cash, and other metrics 34 of a portfolio at a specific point in time. It is updated within a loop by the 35 Builder class during the simulation process. 36 37 The class provides properties for accessing various portfolio metrics like 38 cash, NAV, value, weights, and leverage. It also provides setter methods 39 for updating the portfolio state (aum, cash, position, prices). 40 41 Attributes 42 ---------- 43 _prices : pd.Series 44 Current prices of assets in the portfolio 45 _position : pd.Series 46 Current positions (units) of assets in the portfolio 47 _trades : pd.Series 48 Trades needed to reach the current position 49 _time : datetime 50 Current time in the simulation 51 _days : int 52 Number of days between the current and previous time 53 _profit : float 54 Profit achieved between the previous and current prices 55 _aum : float 56 Current assets under management (AUM) of the portfolio 57 58 """ 59 60 _prices: pd.Series | None = None 61 _position: pd.Series | None = None 62 _trades: pd.Series | None = None 63 _time: datetime | None = None 64 _days: int = 0 65 _profit: float = 0.0 66 _aum: float = 0.0 67 68 @property 69 def cash(self) -> float: 70 """Get the current amount of cash available in the portfolio. 71 72 Returns 73 ------- 74 float 75 The cash component of the portfolio, calculated as NAV minus 76 the value of all positions 77 78 """ 79 return self.nav - self.value 80 81 @cash.setter 82 def cash(self, cash: float) -> None: 83 """Update the amount of cash available in the portfolio. 84 85 This updates the AUM (assets under management) based on the new 86 cash amount while keeping the value of positions constant. 87 88 Parameters 89 ---------- 90 cash : float 91 The new cash amount to set 92 93 """ 94 self.aum = cash + self.value 95 96 @property 97 def nav(self) -> float: 98 """Get the net asset value (NAV) of the portfolio. 99 100 The NAV represents the total value of the portfolio, including 101 both the value of positions and available cash. 102 103 Returns 104 ------- 105 float 106 The net asset value of the portfolio 107 108 Notes 109 ----- 110 This is equivalent to the AUM (assets under management). 111 112 """ 113 # assert np.isclose(self.value + self.cash, self.aum), f"{self.value + self.cash} != {self.aum}" 114 # return self.value + self.cash 115 return self.aum 116 117 @property 118 def value(self) -> float: 119 """Get the value of all positions in the portfolio. 120 121 This computes the total value of all holdings at current prices, 122 not including cash. 123 124 Returns 125 ------- 126 float 127 The sum of values of all positions 128 129 Notes 130 ----- 131 If positions are missing (None), the sum will effectively be zero. 132 133 """ 134 return self.cashposition.sum() 135 136 @property 137 def cashposition(self) -> pd.Series: 138 """Get the cash value of each position in the portfolio. 139 140 This computes the cash value of each position by multiplying 141 the number of units by the current price for each asset. 142 143 Returns 144 ------- 145 pd.Series 146 Series with the cash value of each position, indexed by asset 147 148 """ 149 return self.prices * self.position 150 151 @property 152 def position(self) -> pd.Series: 153 """Get the current position (number of units) for each asset. 154 155 Returns 156 ------- 157 pd.Series 158 Series with the number of units held for each asset, indexed by asset. 159 If the position is not yet set, returns an empty series with the 160 correct index. 161 162 """ 163 if self._position is None: 164 return pd.Series(index=self.assets, dtype=float) 165 166 return self._position 167 168 @position.setter 169 def position(self, position: np.ndarray | pd.Series) -> None: 170 """Update the position of the portfolio. 171 172 This method updates the position (number of units) for each asset, 173 computes the required trades to reach the new position, and updates 174 the internal state. 175 176 Parameters 177 ---------- 178 position : Union[np.ndarray, pd.Series] 179 The new position to set, either as a numpy array or pandas Series. 180 If a numpy array, it must have the same length as self.assets. 181 182 """ 183 # update the position 184 position = pd.Series(index=self.assets, data=position) 185 186 # compute the trades (can be fractional) 187 self._trades = position.subtract(self.position, fill_value=0.0) 188 189 # update only now as otherwise the trades would be wrong 190 self._position = position 191 192 @property 193 def gmv(self) -> float: 194 """Get the gross market value of the portfolio. 195 196 The gross market value is the sum of the absolute values of all positions, 197 which represents the total market exposure including both long and short positions. 198 199 Returns 200 ------- 201 float 202 The gross market value (abs(short) + long) 203 204 """ 205 return self.cashposition.abs().sum() 206 207 @property 208 def time(self) -> datetime | None: 209 """Get the current time of the portfolio state. 210 211 Returns 212 ------- 213 Optional[datetime] 214 The current time in the simulation, or None if not set 215 216 """ 217 return self._time 218 219 @time.setter 220 def time(self, time: datetime) -> None: 221 """Update the time of the portfolio state. 222 223 This method updates the current time and computes the number of days 224 between the new time and the previous time. 225 226 Parameters 227 ---------- 228 time : datetime 229 The new time to set 230 231 """ 232 if self.time is None: 233 self._days = 0 234 self._time = time 235 else: 236 self._days = (time - self.time).days 237 self._time = time 238 239 @property 240 def days(self) -> int: 241 """Get the number of days between the current and previous time. 242 243 Returns 244 ------- 245 int 246 Number of days between the current and previous time 247 248 Notes 249 ----- 250 This is useful for computing interest when holding cash or for 251 time-dependent calculations. 252 253 """ 254 return self._days 255 256 @property 257 def assets(self) -> pd.Index: 258 """Get the assets currently in the portfolio. 259 260 Returns 261 ------- 262 pd.Index 263 Index of assets with valid prices in the portfolio. 264 If no prices are set, returns an empty index. 265 266 """ 267 if self._prices is None: 268 return pd.Index(data=[], dtype=str) 269 270 return self.prices.dropna().index 271 272 @property 273 def trades(self) -> pd.Series | None: 274 """Get the trades needed to reach the current position. 275 276 Returns 277 ------- 278 Optional[pd.Series] 279 Series of trades (changes in position) needed to reach the current position. 280 None if no trades have been calculated yet. 281 282 Notes 283 ----- 284 This is helpful when computing trading costs following a position change. 285 Positive values represent buys, negative values represent sells. 286 287 """ 288 return self._trades 289 290 @property 291 def mask(self) -> np.ndarray: 292 """Get a boolean mask for assets with valid (non-NaN) prices. 293 294 Returns 295 ------- 296 np.ndarray 297 Boolean array where True indicates a valid price and False indicates 298 a missing (NaN) price. Returns an empty array if no prices are set. 299 300 """ 301 if self._prices is None: 302 return np.empty(0, dtype=bool) 303 304 return np.isfinite(self.prices.values) 305 306 @property 307 def prices(self) -> pd.Series: 308 """Get the current prices of assets in the portfolio. 309 310 Returns 311 ------- 312 pd.Series 313 Series of current prices indexed by asset. 314 Returns an empty series if no prices are set. 315 316 """ 317 if self._prices is None: 318 return pd.Series(dtype=float) 319 return self._prices 320 321 @prices.setter 322 def prices(self, prices: pd.Series | dict) -> None: 323 """Update the prices of assets in the portfolio. 324 325 This method updates the prices and calculates the profit achieved 326 due to price changes. It also updates the portfolio's AUM by adding 327 the profit. 328 329 Parameters 330 ---------- 331 prices : pd.Series 332 New prices for assets in the portfolio 333 334 Notes 335 ----- 336 The profit is calculated as the difference between the portfolio value 337 before and after the price update. 338 339 """ 340 value_before = (self.prices * self.position).sum() # self.cashposition.sum() 341 value_after = (prices * self.position).sum() 342 343 self._prices = prices 344 self._profit = value_after - value_before 345 self.aum += self.profit 346 347 @property 348 def profit(self) -> float: 349 """Get the profit achieved between the previous and current prices. 350 351 Returns 352 ------- 353 float 354 The profit (or loss) achieved due to price changes since the 355 last price update 356 357 """ 358 return self._profit 359 360 @property 361 def aum(self) -> float: 362 """Get the current assets under management (AUM) of the portfolio. 363 364 Returns 365 ------- 366 float 367 The total assets under management 368 369 """ 370 return self._aum 371 372 @aum.setter 373 def aum(self, aum: float) -> None: 374 """Update the assets under management (AUM) of the portfolio. 375 376 Parameters 377 ---------- 378 aum : float 379 The new assets under management value to set 380 381 """ 382 self._aum = aum 383 384 @property 385 def weights(self) -> pd.Series: 386 """Get the weight of each asset in the portfolio. 387 388 This computes the weighting of each asset as a fraction of the 389 total portfolio value (NAV). 390 391 Returns 392 ------- 393 pd.Series 394 Series containing the weight of each asset as a fraction of the 395 total portfolio value, indexed by asset 396 397 Notes 398 ----- 399 If positions are missing, a series of zeros is effectively returned. 400 The sum of weights equals 1.0 for a fully invested portfolio with no leverage. 401 402 """ 403 if not np.isclose(self.nav, self.aum): 404 raise ValueError(f"{self.nav} != {self.aum}") 405 406 return self.cashposition / self.nav 407 408 @property 409 def leverage(self) -> float: 410 """Get the leverage of the portfolio. 411 412 Leverage is calculated as the sum of the absolute values of all position 413 weights. For a long-only portfolio with no cash, this equals 1.0. 414 For a portfolio with shorts or leverage, this will be greater than 1.0. 415 416 Returns 417 ------- 418 float 419 The leverage ratio of the portfolio 420 421 Notes 422 ----- 423 A leverage of 2.0 means the portfolio has twice the market exposure 424 compared to its net asset value, which could be achieved through 425 borrowing or short selling. 426 427 """ 428 return float(self.weights.abs().sum())
Represents the current state of a portfolio during simulation.
The State class tracks the current positions, prices, cash, and other metrics of a portfolio at a specific point in time. It is updated within a loop by the Builder class during the simulation process.
The class provides properties for accessing various portfolio metrics like cash, NAV, value, weights, and leverage. It also provides setter methods for updating the portfolio state (aum, cash, position, prices).
Attributes
_prices : pd.Series Current prices of assets in the portfolio _position : pd.Series Current positions (units) of assets in the portfolio _trades : pd.Series Trades needed to reach the current position _time : datetime Current time in the simulation _days : int Number of days between the current and previous time _profit : float Profit achieved between the previous and current prices _aum : float Current assets under management (AUM) of the portfolio
68 @property 69 def cash(self) -> float: 70 """Get the current amount of cash available in the portfolio. 71 72 Returns 73 ------- 74 float 75 The cash component of the portfolio, calculated as NAV minus 76 the value of all positions 77 78 """ 79 return self.nav - self.value
Get the current amount of cash available in the portfolio.
Returns
float The cash component of the portfolio, calculated as NAV minus the value of all positions
117 @property 118 def value(self) -> float: 119 """Get the value of all positions in the portfolio. 120 121 This computes the total value of all holdings at current prices, 122 not including cash. 123 124 Returns 125 ------- 126 float 127 The sum of values of all positions 128 129 Notes 130 ----- 131 If positions are missing (None), the sum will effectively be zero. 132 133 """ 134 return self.cashposition.sum()
Get the value of all positions in the portfolio.
This computes the total value of all holdings at current prices, not including cash.
Returns
float The sum of values of all positions
Notes
If positions are missing (None), the sum will effectively be zero.
136 @property 137 def cashposition(self) -> pd.Series: 138 """Get the cash value of each position in the portfolio. 139 140 This computes the cash value of each position by multiplying 141 the number of units by the current price for each asset. 142 143 Returns 144 ------- 145 pd.Series 146 Series with the cash value of each position, indexed by asset 147 148 """ 149 return self.prices * self.position
Get the cash value of each position in the portfolio.
This computes the cash value of each position by multiplying the number of units by the current price for each asset.
Returns
pd.Series Series with the cash value of each position, indexed by asset
151 @property 152 def position(self) -> pd.Series: 153 """Get the current position (number of units) for each asset. 154 155 Returns 156 ------- 157 pd.Series 158 Series with the number of units held for each asset, indexed by asset. 159 If the position is not yet set, returns an empty series with the 160 correct index. 161 162 """ 163 if self._position is None: 164 return pd.Series(index=self.assets, dtype=float) 165 166 return self._position
Get the current position (number of units) for each asset.
Returns
pd.Series Series with the number of units held for each asset, indexed by asset. If the position is not yet set, returns an empty series with the correct index.
192 @property 193 def gmv(self) -> float: 194 """Get the gross market value of the portfolio. 195 196 The gross market value is the sum of the absolute values of all positions, 197 which represents the total market exposure including both long and short positions. 198 199 Returns 200 ------- 201 float 202 The gross market value (abs(short) + long) 203 204 """ 205 return self.cashposition.abs().sum()
Get the gross market value of the portfolio.
The gross market value is the sum of the absolute values of all positions, which represents the total market exposure including both long and short positions.
Returns
float The gross market value (abs(short) + long)
207 @property 208 def time(self) -> datetime | None: 209 """Get the current time of the portfolio state. 210 211 Returns 212 ------- 213 Optional[datetime] 214 The current time in the simulation, or None if not set 215 216 """ 217 return self._time
Get the current time of the portfolio state.
Returns
Optional[datetime] The current time in the simulation, or None if not set
239 @property 240 def days(self) -> int: 241 """Get the number of days between the current and previous time. 242 243 Returns 244 ------- 245 int 246 Number of days between the current and previous time 247 248 Notes 249 ----- 250 This is useful for computing interest when holding cash or for 251 time-dependent calculations. 252 253 """ 254 return self._days
Get the number of days between the current and previous time.
Returns
int Number of days between the current and previous time
Notes
This is useful for computing interest when holding cash or for time-dependent calculations.
256 @property 257 def assets(self) -> pd.Index: 258 """Get the assets currently in the portfolio. 259 260 Returns 261 ------- 262 pd.Index 263 Index of assets with valid prices in the portfolio. 264 If no prices are set, returns an empty index. 265 266 """ 267 if self._prices is None: 268 return pd.Index(data=[], dtype=str) 269 270 return self.prices.dropna().index
Get the assets currently in the portfolio.
Returns
pd.Index Index of assets with valid prices in the portfolio. If no prices are set, returns an empty index.
272 @property 273 def trades(self) -> pd.Series | None: 274 """Get the trades needed to reach the current position. 275 276 Returns 277 ------- 278 Optional[pd.Series] 279 Series of trades (changes in position) needed to reach the current position. 280 None if no trades have been calculated yet. 281 282 Notes 283 ----- 284 This is helpful when computing trading costs following a position change. 285 Positive values represent buys, negative values represent sells. 286 287 """ 288 return self._trades
Get the trades needed to reach the current position.
Returns
Optional[pd.Series] Series of trades (changes in position) needed to reach the current position. None if no trades have been calculated yet.
Notes
This is helpful when computing trading costs following a position change. Positive values represent buys, negative values represent sells.
290 @property 291 def mask(self) -> np.ndarray: 292 """Get a boolean mask for assets with valid (non-NaN) prices. 293 294 Returns 295 ------- 296 np.ndarray 297 Boolean array where True indicates a valid price and False indicates 298 a missing (NaN) price. Returns an empty array if no prices are set. 299 300 """ 301 if self._prices is None: 302 return np.empty(0, dtype=bool) 303 304 return np.isfinite(self.prices.values)
Get a boolean mask for assets with valid (non-NaN) prices.
Returns
np.ndarray Boolean array where True indicates a valid price and False indicates a missing (NaN) price. Returns an empty array if no prices are set.
306 @property 307 def prices(self) -> pd.Series: 308 """Get the current prices of assets in the portfolio. 309 310 Returns 311 ------- 312 pd.Series 313 Series of current prices indexed by asset. 314 Returns an empty series if no prices are set. 315 316 """ 317 if self._prices is None: 318 return pd.Series(dtype=float) 319 return self._prices
Get the current prices of assets in the portfolio.
Returns
pd.Series Series of current prices indexed by asset. Returns an empty series if no prices are set.
347 @property 348 def profit(self) -> float: 349 """Get the profit achieved between the previous and current prices. 350 351 Returns 352 ------- 353 float 354 The profit (or loss) achieved due to price changes since the 355 last price update 356 357 """ 358 return self._profit
Get the profit achieved between the previous and current prices.
Returns
float The profit (or loss) achieved due to price changes since the last price update
360 @property 361 def aum(self) -> float: 362 """Get the current assets under management (AUM) of the portfolio. 363 364 Returns 365 ------- 366 float 367 The total assets under management 368 369 """ 370 return self._aum
Get the current assets under management (AUM) of the portfolio.
Returns
float The total assets under management
384 @property 385 def weights(self) -> pd.Series: 386 """Get the weight of each asset in the portfolio. 387 388 This computes the weighting of each asset as a fraction of the 389 total portfolio value (NAV). 390 391 Returns 392 ------- 393 pd.Series 394 Series containing the weight of each asset as a fraction of the 395 total portfolio value, indexed by asset 396 397 Notes 398 ----- 399 If positions are missing, a series of zeros is effectively returned. 400 The sum of weights equals 1.0 for a fully invested portfolio with no leverage. 401 402 """ 403 if not np.isclose(self.nav, self.aum): 404 raise ValueError(f"{self.nav} != {self.aum}") 405 406 return self.cashposition / self.nav
Get the weight of each asset in the portfolio.
This computes the weighting of each asset as a fraction of the total portfolio value (NAV).
Returns
pd.Series Series containing the weight of each asset as a fraction of the total portfolio value, indexed by asset
Notes
If positions are missing, a series of zeros is effectively returned. The sum of weights equals 1.0 for a fully invested portfolio with no leverage.
408 @property 409 def leverage(self) -> float: 410 """Get the leverage of the portfolio. 411 412 Leverage is calculated as the sum of the absolute values of all position 413 weights. For a long-only portfolio with no cash, this equals 1.0. 414 For a portfolio with shorts or leverage, this will be greater than 1.0. 415 416 Returns 417 ------- 418 float 419 The leverage ratio of the portfolio 420 421 Notes 422 ----- 423 A leverage of 2.0 means the portfolio has twice the market exposure 424 compared to its net asset value, which could be achieved through 425 borrowing or short selling. 426 427 """ 428 return float(self.weights.abs().sum())
Get the leverage of the portfolio.
Leverage is calculated as the sum of the absolute values of all position weights. For a long-only portfolio with no cash, this equals 1.0. For a portfolio with shorts or leverage, this will be greater than 1.0.
Returns
float The leverage ratio of the portfolio
Notes
A leverage of 2.0 means the portfolio has twice the market exposure compared to its net asset value, which could be achieved through borrowing or short selling.
25def interpolate(ts): 26 """Interpolate missing values in a time series between the first and last valid indices. 27 28 This function fills forward (ffill) missing values in a time series, but only 29 between the first and last valid indices. Values outside this range remain NaN/null. 30 31 Parameters 32 ---------- 33 ts : pd.Series or pl.Series 34 The time series to interpolate 35 36 Returns 37 ------- 38 pd.Series or pl.Series 39 The interpolated time series 40 41 Examples 42 -------- 43 >>> import pandas as pd 44 >>> import numpy as np 45 >>> ts = pd.Series([1, np.nan, np.nan, 4, 5]) 46 >>> interpolate(ts) 47 0 1.0 48 1 1.0 49 2 1.0 50 3 4.0 51 4 5.0 52 dtype: float64 53 54 """ 55 # Check if the input is a valid type 56 if not isinstance(ts, pd.Series | pl.Series): 57 raise TypeError(f"Expected pd.Series or pl.Series, got {type(ts)}") 58 59 # If the input is a polars Series, use the polars-specific function 60 if isinstance(ts, pl.Series): 61 return interpolate_pl(ts) 62 first = ts.first_valid_index() 63 last = ts.last_valid_index() 64 65 if first is not None and last is not None: 66 ts_slice = ts.loc[first:last] 67 ts_slice = ts_slice.ffill() 68 result = ts.copy() 69 result.loc[first:last] = ts_slice 70 return result 71 return ts
Interpolate missing values in a time series between the first and last valid indices.
This function fills forward (ffill) missing values in a time series, but only between the first and last valid indices. Values outside this range remain NaN/null.
Parameters
ts : pd.Series or pl.Series The time series to interpolate
Returns
pd.Series or pl.Series The interpolated time series
Examples
>>> import pandas as pd
>>> import numpy as np
>>> ts = pd.Series([1, np.nan, np.nan, 4, 5])
>>> interpolate(ts)
0 1.0
1 1.0
2 1.0
3 4.0
4 5.0
dtype: float64
74def valid(ts) -> bool: 75 """Check if a time series has no missing values between the first and last valid indices. 76 77 This function verifies that a time series doesn't have any NaN/null values in the middle. 78 It's acceptable to have NaNs/nulls at the beginning or end of the series. 79 80 Parameters 81 ---------- 82 ts : pd.Series or pl.Series 83 The time series to check 84 85 Returns 86 ------- 87 bool 88 True if the time series has no missing values between the first and last valid indices, 89 False otherwise 90 91 Examples 92 -------- 93 >>> import pandas as pd 94 >>> import numpy as np 95 >>> ts1 = pd.Series([np.nan, 1, 2, 3, np.nan]) # NaNs only at beginning and end 96 >>> valid(ts1) 97 True 98 >>> ts2 = pd.Series([1, 2, np.nan, 4, 5]) # NaN in the middle 99 >>> valid(ts2) 100 False 101 102 """ 103 # Check if the input is a valid type 104 if not isinstance(ts, pd.Series | pl.Series): 105 raise TypeError(f"Expected pd.Series or pl.Series, got {type(ts)}") 106 107 # If the input is a polars Series, use the polars-specific function 108 if isinstance(ts, pl.Series): 109 return valid_pl(ts) 110 # Check if the series with NaNs dropped has the same indices as the interpolated series with NaNs dropped 111 # If they're the same, there are no NaNs in the middle of the series 112 return ts.dropna().index.equals(interpolate(ts).dropna().index)
Check if a time series has no missing values between the first and last valid indices.
This function verifies that a time series doesn't have any NaN/null values in the middle. It's acceptable to have NaNs/nulls at the beginning or end of the series.
Parameters
ts : pd.Series or pl.Series The time series to check
Returns
bool True if the time series has no missing values between the first and last valid indices, False otherwise
Examples
>>> import pandas as pd
>>> import numpy as np
>>> ts1 = pd.Series([np.nan, 1, 2, 3, np.nan]) # NaNs only at beginning and end
>>> valid(ts1)
True
>>> ts2 = pd.Series([1, 2, np.nan, 4, 5]) # NaN in the middle
>>> valid(ts2)
False