Skip to content

Portfolio

Instance of a portfolio, which is the main entry point to the library.

Portfolio

Portfolio(
    data: DataFrame,
    input_type: str = "prices",
    weights: Series | ndarray | dict | None = None,
    name: str = "My Portfolio",
    frequency: int = TRADING_DAYS_PER_YEAR,
    risk_free_rate: float = DEFAULT_RISK_FREE_RATE,
    asset_classes: dict[str, AssetClass] | None = None,
    base_currency: str | None = None,
)

A collection of assets, their prices, and their weights, PortPy's main entry point.

Parameters:

Name Type Description Default
data DataFrame

Columns = asset symbols, index = a sorted, duplicate-free DatetimeIndex. See :func:portpy.core.align_calendars if you're combining assets with different trading calendars (e.g. crypto and equities), and :func:portpy.core.convert_to_base_currency if your assets aren't all in the same currency - both run before constructing the Portfolio, which never modifies your data itself.

required
input_type str

"prices" (default) or "returns" - what data contains.

'prices'
weights Series | ndarray | dict | None

Optional weights (Series/dict/array, any order); defaults to equal-weight (1/N) across all columns in data.

None
name str

A label used in chart titles and comparisons.

'My Portfolio'
frequency int

Trading periods per year, used to annualize metrics (default 252 for daily equity data; use 365 for a crypto-only, 7-day-a-week portfolio, 12 for monthly data, etc.).

TRADING_DAYS_PER_YEAR
risk_free_rate float

Annual risk-free rate, used as the default rf in .metrics calls.

DEFAULT_RISK_FREE_RATE
asset_classes dict[str, AssetClass] | None

Optional {symbol: AssetClass} tags, purely for reporting/grouping - see portpy.core.AssetClass.

None
base_currency str | None

Optional 3-letter currency code (e.g. "USD"), purely informational/for reporting - like asset_classes, it's never used to convert anything. data must already be in a single currency by the time it reaches Portfolio; run portpy.core.convert_to_base_currency yourself first if it isn't, then pass the currency you converted to here as a label.

None
Source code in src/portpy/portfolio.py
def __init__(
    self,
    data: pd.DataFrame,
    input_type: str = "prices",
    weights: pd.Series | np.ndarray | dict | None = None,
    name: str = "My Portfolio",
    frequency: int = TRADING_DAYS_PER_YEAR,
    risk_free_rate: float = DEFAULT_RISK_FREE_RATE,
    asset_classes: dict[str, AssetClass] | None = None,
    base_currency: str | None = None,
) -> None:
    if not isinstance(data, pd.DataFrame):
        raise TypeError(
            f"Portfolio expects a pandas DataFrame (columns = asset symbols), got {type(data).__name__}."
        )
    if data.shape[1] == 0:
        raise ValueError("data has no asset columns.")
    ensure_datetime_index(data, name="data")
    ensure_min_observations(data, 2, "data")
    if input_type not in _INPUT_TYPES:
        raise ValueError(f"input_type must be one of {_INPUT_TYPES}, got {input_type!r}.")

    if input_type == "prices":
        self._prices = data.astype(float).copy()
    else:
        self._prices = prices_from_returns(data.astype(float))

    self._asset_names = list(data.columns)
    self.name = name
    self.frequency = int(frequency)
    self.risk_free_rate = float(risk_free_rate)

    if asset_classes:
        unknown = set(asset_classes) - set(self._asset_names)
        if unknown:
            raise ValueError(f"asset_classes references unknown assets: {sorted(unknown)}")
    self.asset_classes: dict[str, AssetClass] = dict(asset_classes or {})
    self.base_currency: str | None = base_currency

    self.set_weights(weights if weights is not None else equal_weights(self._asset_names))
    self.metrics = _MetricsNamespace(self)
    self.models = _ModelsNamespace(self)

prices property

prices: DataFrame

Asset price DataFrame (reconstructed from returns at construction time if needed).

asset_names property

asset_names: list[str]

The portfolio's asset symbols, in column order.

num_assets property

num_assets: int

The number of assets in the portfolio.

weights property

weights: Series

The current per-asset weights, indexed by asset name.

set_weights

set_weights(weights: Series | ndarray | dict) -> None

Validate, normalize (sum to 1), and apply new portfolio weights.

Parameters:

Name Type Description Default
weights Series | ndarray | dict

A Series/dict keyed by asset name, or a plain array aligned with asset_names.

required
Source code in src/portpy/portfolio.py
def set_weights(self, weights: pd.Series | np.ndarray | dict) -> None:
    """
    Validate, normalize (sum to 1), and apply new portfolio weights.

    Args:
        weights: A Series/dict keyed by asset name, or a plain array aligned
            with `asset_names`.
    """
    self._weights = normalize_weights(weights, names=self._asset_names)

set_risk_free_rate

set_risk_free_rate(rate: float) -> None

Set the annual risk-free rate used as the default rf in .metrics calls.

Parameters:

Name Type Description Default
rate float

The new annual risk-free rate (e.g. 0.04 for 4%/year).

required
Source code in src/portpy/portfolio.py
def set_risk_free_rate(self, rate: float) -> None:
    """
    Set the annual risk-free rate used as the default rf in .metrics calls.

    Args:
        rate: The new annual risk-free rate (e.g. 0.04 for 4%/year).
    """
    self.risk_free_rate = float(rate)

asset_returns

asset_returns(log: bool = False) -> pd.DataFrame

Per-asset simple (or log) returns - the multi-asset DataFrame, not the portfolio aggregate.

Parameters:

Name Type Description Default
log bool

If True, return log returns instead of simple returns.

False

Returns:

Type Description
DataFrame

A DataFrame of per-period returns, one column per asset.

Source code in src/portpy/portfolio.py
def asset_returns(self, log: bool = False) -> pd.DataFrame:
    """
    Per-asset simple (or log) returns - the multi-asset DataFrame, not the portfolio aggregate.

    Args:
        log: If True, return log returns instead of simple returns.

    Returns:
        A DataFrame of per-period returns, one column per asset.
    """
    return log_returns(self._prices) if log else simple_returns(self._prices)

returns

returns(period: int = 1, log: bool = False) -> pd.Series

The portfolio's own return series: asset returns combined by current weights.

Assumes weights are held constant each period (rebalanced back to target every period). For turnover/rebalancing effects, see :mod:portpy.models.management and (eventually) :mod:portpy.strategies.

Parameters:

Name Type Description Default
period int

Compound returns over non-overlapping blocks of this many periods (e.g. period=21 on daily data for a rough monthly series) instead of period-over-period.

1
log bool

If True, return log returns instead of simple returns.

False

Returns:

Type Description
Series

The portfolio's return series, named after the portfolio.

Source code in src/portpy/portfolio.py
def returns(self, period: int = 1, log: bool = False) -> pd.Series:
    """
    The portfolio's own return series: asset returns combined by current weights.

    Assumes weights are held constant each period (rebalanced back to target
    every period). For turnover/rebalancing effects, see :mod:portpy.models.management
    and (eventually) :mod:portpy.strategies.

    Args:
        period: Compound returns over non-overlapping blocks of this many
            periods (e.g. period=21 on daily data for a rough monthly
            series) instead of period-over-period.
        log: If True, return log returns instead of simple returns.

    Returns:
        The portfolio's return series, named after the portfolio.
    """
    asset_r = self.asset_returns(log=False)
    port_r = (asset_r * self._weights).sum(axis=1)
    port_r.name = self.name

    if period > 1:
        block = np.arange(len(port_r)) // period
        compounded = (1.0 + port_r).groupby(block).prod() - 1.0
        block_end_dates = port_r.index.to_series().groupby(block).last()
        compounded.index = block_end_dates.to_numpy()
        port_r = compounded

    if log:
        port_r = np.log1p(port_r)
    return port_r.rename(self.name)

price_index

price_index(base: float = 100.0) -> pd.Series

Synthetic aggregated portfolio value, rebased to base, built by compounding .returns().

Parameters:

Name Type Description Default
base float

The starting value of the index.

100.0

Returns:

Type Description
Series

A synthetic price series, named after the portfolio, anchored one

Series

period before the first return.

Source code in src/portpy/portfolio.py
def price_index(self, base: float = 100.0) -> pd.Series:
    """
    Synthetic aggregated portfolio value, rebased to base, built by compounding .returns().

    Args:
        base: The starting value of the index.

    Returns:
        A synthetic price series, named after the portfolio, anchored one
        period before the first return.
    """
    r = self.returns()
    return prices_from_returns(r, base=base).rename(self.name)