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]
@dataclass
class Builder:
 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.

Builder( prices: pandas.core.frame.DataFrame, _state: State | None = None, _units: pandas.core.frame.DataFrame | None = None, _aum: pandas.core.series.Series | None = None, initial_aum: float = 1000000.0)
prices: pandas.core.frame.DataFrame
initial_aum: float = 1000000.0
valid
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.

intervals
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.

index: pandas.core.indexes.datetimes.DatetimeIndex
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.

current_prices: numpy.ndarray
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.

position: pandas.core.series.Series
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.

cashposition
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.

units
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.

def build(self):
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.

weights: numpy.ndarray
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.

aum
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.

@dataclass(frozen=True)
class Portfolio:
 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

Portfolio( prices: pandas.core.frame.DataFrame, units: pandas.core.frame.DataFrame, aum: float | pandas.core.series.Series)
prices: pandas.core.frame.DataFrame
units: pandas.core.frame.DataFrame
aum: float | pandas.core.series.Series
index: list[datetime.datetime]
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.

assets: list[str]
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.

nav: pandas.core.series.Series
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

Get the net asset value (NAV) of the portfolio over time.

The NAV represents the total value of the portfolio at each point in time. If aum is provided as a Series, it is used directly. Otherwise, the NAV is calculated from the cumulative profit plus the initial aum.

Returns

pd.Series Series representing the NAV of the portfolio over time

profit: pandas.core.series.Series
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.

cashposition: pandas.core.frame.DataFrame
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

returns: pandas.core.frame.DataFrame
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

trades_units: pandas.core.frame.DataFrame
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.

trades_currency: pandas.core.frame.DataFrame
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).

turnover_relative: pandas.core.frame.DataFrame
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.

turnover: pandas.core.frame.DataFrame
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.

equity: pandas.core.frame.DataFrame
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.

weights: pandas.core.frame.DataFrame
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.

stats
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.

plots
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.

reports
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.

def sharpe(self, periods=None):
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.

@classmethod
def from_cashpos_prices( cls, prices: pandas.core.frame.DataFrame, cashposition: pandas.core.frame.DataFrame, aum: float):
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.

def snapshot(self, title: str = 'Portfolio Summary', log_scale: bool = True):
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.

@dataclass()
class State:
 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

State( _prices: pandas.core.series.Series | None = None, _position: pandas.core.series.Series | None = None, _trades: pandas.core.series.Series | None = None, _time: datetime.datetime | None = None, _days: int = 0, _profit: float = 0.0, _aum: float = 0.0)
cash: float
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

nav: float
 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

Get the net asset value (NAV) of the portfolio.

The NAV represents the total value of the portfolio, including both the value of positions and available cash.

Returns

float The net asset value of the portfolio

Notes

This is equivalent to the AUM (assets under management).

value: float
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.

cashposition: pandas.core.series.Series
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

position: pandas.core.series.Series
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.

gmv: float
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)

time: datetime.datetime | None
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

days: int
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.

assets: pandas.core.indexes.base.Index
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.

trades: pandas.core.series.Series | None
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.

mask: numpy.ndarray
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.

prices: pandas.core.series.Series
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.

profit: float
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

aum: float
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

weights: pandas.core.series.Series
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.

leverage: float
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.

def interpolate(ts):
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
def valid(ts) -> bool:
 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