Note
Go to the end to download the full example code. or to run this example in your browser via Binder
Management Fees#
This tutorial shows how to incorporate management fees (MF) into the
MeanRisk
optimization.
By using The management_fees
parameter, you can add linear MF to the optimization
problem:
with \(f_{i}\) the management fee of asset i and \(w_{i}\) its weight. The float \(total\_fee\) is impacting the portfolio expected return in the optimization:
with \(\mu\) the vector af assets expected returns and \(w\) the vector of assets weights.
The management_fees
parameter can be a float, a dictionary or an array-like of
shape (n_assets, )
. If a float is provided, it is applied to each asset.
If a dictionary is provided, its (key/value) pair must be the (asset name/asset MF) and
the input X
of the fit
method must be a DataFrame with the assets names in
columns. The default is 0.0 (no management fees).
Note
Another approach is to direcly impact the MF to the input X
in order to express
the returns net of fee. However, when estimating the \(\mu\) parameter using,
for example, Shrinkage estimators, this approach would mix a deterministic amount
with an uncertain one leading to unwanted bias in the management fees.
Data#
We load the S&P 500 dataset composed of the daily prices of 20 assets from the S&P 500 Index composition starting from 1990-01-02 up to 2022-12-28. We select only 3 assets to make the example more readable, which are Apple (AAPL), General Electric (GE) and JPMorgan (JPM).
import numpy as np
from plotly.io import show
from skfolio import Population
from skfolio.datasets import load_sp500_dataset
from skfolio.model_selection import WalkForward, cross_val_predict
from skfolio.optimization import MeanRisk, ObjectiveFunction
from skfolio.preprocessing import prices_to_returns
prices = load_sp500_dataset()
prices = prices[["AAPL", "GE", "JPM"]]
X = prices_to_returns(prices)
Model#
In this tutorial, we will use the Maximum Mean-Variance Utility model with a risk aversion of 1.0:
model = MeanRisk(objective_function=ObjectiveFunction.MAXIMIZE_UTILITY)
model.fit(X)
model.weights_
array([6.17733231e-01, 3.78775169e-09, 3.82266765e-01])
Management Fees#
Management fees are usually used in assets under management but for this example we will assume that it also applies for the below stocks:
Apple: 3% p.a.
General Electric: 6% p.a.
JPMorgan: 1% p.a.
The MF are expressed in per annum, so we need to convert them in daily MF. We suppose 252 trading days in a year:
management_fees = {"AAPL": 0.03 / 252, "GE": 0.06 / 252, "JPM": 0.01 / 252}
# Same as management_fees = np.array([0.03, 0.06, 0.01]) / 252
model_mf = MeanRisk(
objective_function=ObjectiveFunction.MAXIMIZE_UTILITY,
management_fees=management_fees,
)
model_mf.fit(X)
model_mf.weights_
array([5.74787861e-01, 1.43028265e-08, 4.25212125e-01])
The higher MF of Apple induced a change of weights toward JPMorgan:
model_mf.weights_ - model.weights_
array([-4.29453703e-02, 1.05150748e-08, 4.29453598e-02])
Multi-period portfolio#
Let’s assume that we want to rebalance our portfolio every 60 days by re-fitting the model on the latest 60 days. We test the impact of MF using Walk Forward Analysis:
holding_period = 60
fitting_period = 60
cv = WalkForward(train_size=fitting_period, test_size=holding_period)
As explained above, we transform the yearly MF into a daily MF:
management_fees = np.array([0.03, 0.06, 0.01]) / 252
First, we train the model without MF and test it with MF.
Note that portfolio_params
are parameters passed to the Portfolio during predict
and not during fit
:
model = MeanRisk(
objective_function=ObjectiveFunction.MAXIMIZE_UTILITY,
portfolio_params=dict(management_fees=management_fees),
)
# pred1 is a MultiPeriodPortfolio
pred1 = cross_val_predict(model, X, cv=cv, n_jobs=-1)
pred1.name = "pred1"
Then, we train and test the model with MF:
model.set_params(management_fees=management_fees)
pred2 = cross_val_predict(model, X, cv=cv, n_jobs=-1)
pred2.name = "pred2"
We visualize the results by plotting the cumulative returns of the successive test periods:
population = Population([pred1, pred2])
fig = population.plot_cumulative_returns()
show(fig)
We notice that the model fitted with MF outperform the model fitted without MF.
Total running time of the script: (0 minutes 4.075 seconds)