Portfolio#

Portfolio classes implement a large set of attributes and methods intended for portfolio analysis. They are returned by the predict method of portfolio optimizations.

They are also data-containers (calling np.asarray(portfolio) returns the portfolio returns) making them compatible with sklearn.model_selection tools.

They use slots for improved performance.

Base Portfolio#

BasePortfolio directly takes a portfolio returns array as input and implements a large set of attributes and methods.

Example:

import datetime as dt
from skfolio import BasePortfolio

portfolio = BasePortfolio(
    returns=[0.002, -0.001, 0.0015],
    observations=[dt.date(2022, 1, 1), dt.date(2022, 1, 2), dt.date(2022, 1, 3)],
)

Attributes and Methods#

More than 40 attributes and methods are available, including all the measures (Mean, Variance, Sharpe Ratio, CVaR, CDaR, Drawdowns, etc.). The attributes are computed only when requested, then cached in slots for enhanced performance.

Measures are computed on the per-observation return series, in the periodicity of the returns. The annualized variants (e.g. annualized_sharpe_ratio, annualized_mean) scale them for reporting using the annualization_factor parameter (default 252). Optimization inputs are never annualized, only reported measures are (see Periodicity Convention).

Example:

from skfolio import RatioMeasure

# attributes
portfolio.mean
portfolio.variance
portfolio.sharpe_ratio
portfolio.sortino_ratio
portfolio.cdar
portfolio.max_drawdown
portfolio.cumulative_returns
portfolio.drawdowns
portfolio.returns_df
portfolio.cumulative_returns_df

# methods
portfolio.summary()
portfolio.dominates(other_portfolio)
portfolio.rolling_measure(measure=RatioMeasure.SHARPE_RATIO)

# plots
portfolio.plot_cumulative_returns()
portfolio.plot_rolling_measure(measure=RatioMeasure.SHARPE_RATIO)

It is also an array container:

np.asarray(portfolio)
>>> array([ 0.002 , -0.001 ,  0.0015])

Finally, portfolios can be compared together using domination:

portfolio == other_portfolio
portfolio >= other_portfolio
portfolio > other_portfolio

The measures used in the domination are controlled using fitness_measures. The default is to use the list [PerfMeasure.MEAN, RiskMeasure.VARIANCE].

Portfolio#

Portfolio inherits from BasePortfolio. Under the default constant-weight convention, portfolio returns are the dot product of the asset weights and asset returns minus costs:

\[r_p = R \cdot w^{T} - c^{T} \cdot |w-w_{prev}| - f^{T} \cdot w\]

with \(r_p\) the vector of portfolio returns, \(R\) the matrix of asset returns, \(w\) the vector of asset weights, \(c\) the vector of asset transaction costs, \(f\) the vector of asset management fees and \(w_{prev}\) the previous asset weights.

By default, each observation is evaluated at the target weights, consistent with the optimizer’s linear portfolio return definition. This convention evaluates allocation skill, the expected return and risk of the selected target allocation, independently of subsequent weight drift.

Because the same target weights are applied to every observation, reordering the observations does not change the resulting distribution of portfolio returns.

Economically, evaluating every observation at the target weights is equivalent to restoring those weights after each observation. The transaction costs of these implicit within-window trades are not charged. The transaction-cost term in the formula above instead applies to the trade from previous_weights to the target weights when the Portfolio is created.

With weight_drift=True, the portfolio starts at the target weights and holds the resulting positions throughout the observation window of X. Position values change with asset returns, so portfolio weights evolve with the relative performance of the assets.

In a long-only portfolio, an asset’s weight increases when its return exceeds the portfolio return and decreases when its return falls below it. More generally, the weights applied to later observations depend on earlier asset returns, so the resulting portfolio return series depends on the order of the observations.

With weight_drift=True and compounded=True, returns computed from drifted weights are compounded into a wealth index for evaluating realized capital growth and other path-dependent measures.

Both weight conventions use the same transaction-cost and management-fee formulas. In a sequential evaluation, transaction costs are computed relative to the previous target weights with weight_drift=False, or the previous period’s ending weights, including drift, with weight_drift=True. Management fees use the target weights under both settings.

weight_drift therefore changes the observation-level portfolio return series, while compounded changes how that series is accumulated. See Backtesting and Evaluation for a detailed discussion of the evaluation objectives and the choice between weight_drift=False and weight_drift=True.

Example:

from skfolio import Portfolio

X = [
    [0.003, -0.001],
    [-0.001, 0.002],
    [0.0015, 0.004],
]

weights = [0.6, 0.4]

portfolio = Portfolio(X=X, weights=weights)

print(portfolio.returns)
>>> array([0.0014, 0.0002, 0.0025])

drifted_portfolio = Portfolio(X=X, weights=weights, weight_drift=True)
drifted_portfolio.weights_per_observation
drifted_portfolio.ending_weights

X can be any data-container including numpy array and pandas DataFrame:

import datetime as dt
import pandas as pd

X = pd.DataFrame(
    data=[[0.003, -0.001], [-0.001, 0.002], [0.0015, 0.004]],
    columns=["Asset A", "Asset B"],
    index=[dt.date(2022, 1, 1), dt.date(2022, 1, 2), dt.date(2022, 1, 3)],
)

print(X)
>>>
            Asset A  Asset B
2022-01-01   0.0030   -0.001
2022-01-02  -0.0010    0.002
2022-01-03   0.0015    0.004

weights = [0.6, 0.4]

portfolio = Portfolio(X=X, weights=weights, name="my_portfolio")

print(portfolio.returns)
>>> array([0.0014, 0.0002, 0.0025])

Attributes and Methods#

Portfolio inherits all the attributes and methods from BasePortfolio. It also provides methods for analyzing portfolio weights:

from skfolio import RatioMeasure

portfolio.contribution(measure=RatioMeasure.ANNUALIZED_SHARPE_RATIO)
>>> array([-3.04203502,  3.04203503])

portfolio.composition
>>>
              my_portfolio
asset
Asset A           0.6
Asset B           0.4

portfolio.get_weight("Asset A")
>>> 0.6

# Weight paths and trading diagnostics
portfolio.weights_per_observation
portfolio.ending_weights
portfolio.turnover

# Plots
portfolio.plot_contribution()
portfolio.plot_composition()

Multi Period Portfolio#

MultiPeriodPortfolio inherits from BasePortfolio and is composed of a list of Portfolio. Its return series concatenates the return series of those portfolios in list order. Its performance and risk measures are computed from that concatenated series.

A MultiPeriodPortfolio is returned by cross_val_predict. Its turnover series holds the turnover of each Portfolio, and ending_weights_dict maps each Portfolio to its weights at the end of its observation window. With weight_drift=False, these are the target weights. With weight_drift=True, they are the held weights after applying the final observation’s asset returns.

For example, calling cross_val_predict with WalkForward will return a MultiPeriodPortfolio composed of multiple test Portfolio, each corresponding to a train/test fold.

from skfolio import MultiPeriodPortfolio

portfolio = MultiPeriodPortfolio(portfolios=[ptf1, ptf2, ptf3])