<a id="sphx-glr-auto-examples-mean-risk-plot-6-transaction-costs-py"></a>

<a id="transaction-costs"></a>

# Transaction Costs

This tutorial shows how to incorporate transaction costs (TC) into the
[`MeanRisk`](https://skfolio.org/generated/skfolio.optimization.MeanRisk.html.md#skfolio.optimization.MeanRisk) optimization.

TC are fixed costs incurred when buying or selling an asset.

By using the `transaction_costs` parameter, you can add linear TC to the optimization
problem:

$$
total\_cost = \sum_{i=1}^{N} c_{i} \times |w_{i} - w\_prev_{i}|

$$

with $c_{i}$ the TC of asset i, $w_{i}$ its weight and $w\_prev_{i}$
its previous weight (defined in `previous_weights`).
The float $total\_cost$ is impacting the portfolio expected return in the
optimization:

$$
expected\_return = \mu^{T} \cdot w - total\_cost

$$

with $\mu$ the vector of expected asset returns and $w$ the vector of
asset weights.

the `transaction_costs` 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 TC) and
the input `X` of the `fit` method must be a DataFrame with the assets names in columns.
The default is 0.0 (no transaction costs).

#### WARNING
According to the above formula, the periodicity of the transaction costs
must match the periodicity of $\mu$. For example, if the input
`X` is composed of **daily** returns, the `transaction_costs` need to be
expressed as **daily** costs.

This means that fixed transaction costs must be converted to an equivalent per-period
cost. The reason is that a transaction cost is paid once per rebalancing while a
position earns its expected return on every period it is held. To convert the
one-off cost, you need the notion of expected investment duration. This is crucial
since the optimization problem itself has no notion of investment duration.

For example, let’s assume that asset A has an expected daily return of 0.01%
with a TC of 1% and asset B has an expected daily return of 0.005% with no TC.
Let’s assume both assets have the same volatility and a correlation of 1.0.
If the investment duration is only one month, we should allocate all the weights to
asset B. However, if the investment duration is one year, we should allocate all the
weights to asset A.

Example:
: * Duration = 1 months (21 business days):
    : * 1 month expected return A ~= -0.8%
      * 1 month expected return B ~= 0.1%
  * Duration = 1 year (252 business days):
    : * 1 year expected return A ~= 1.5%
      * 1 year expected return B ~= 1.3%

So in order to take that duration into account, you should divide the fix TC by the
expected investment duration. See
[Periodicity Convention](https://skfolio.org/user_guide/data_preparation.html.md#periodicity-convention) for the general convention on
expressing optimization inputs in the periodicity of `X`.

<a id="data"></a>

## Data

We load the S&P 500 [dataset](https://skfolio.org/user_guide/datasets.html.md#datasets) 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):

```Python
import numpy as np
from plotly.io import show

from skfolio import MultiPeriodPortfolio, Population, Portfolio
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)
```

<a id="model"></a>

## Model

In this tutorial, we will use the Maximum Mean-Variance Utility model with a risk
aversion of 1.0:

```Python
model = MeanRisk(objective_function=ObjectiveFunction.MAXIMIZE_UTILITY)
model.fit(X)
model.weights_
```

```none
array([6.17733231e-01, 3.78774146e-09, 3.82266765e-01])
```

<a id="transaction-cost"></a>

## Transaction Cost

Let’s assume we have the below TC:
: * Apple: 1%
  * General Electric: 0.50%
  * JPMorgan: 0.20%

and an investment duration of one month (21 business days):

```Python
transaction_costs = {"AAPL": 0.01 / 21, "GE": 0.005 / 21, "JPM": 0.002 / 21}
# Same as transaction_costs = np.array([0.01, 0.005, 0.002]) / 21
```

First, we assume that there is no previous position:

```Python
model_tc = MeanRisk(
    objective_function=ObjectiveFunction.MAXIMIZE_UTILITY,
    transaction_costs=transaction_costs,
)
model_tc.fit(X)
model_tc.weights_
```

```none
array([4.11868007e-01, 1.40979941e-07, 5.88131852e-01])
```

The higher TC of Apple induced a change of weights toward JPMorgan:

```Python
model_tc.weights_ - model.weights_
```

```none
array([-2.05865225e-01,  1.37192199e-07,  2.05865087e-01])
```

Now, let’s assume that the previous position was equal-weighted:

```Python
model_tc2 = MeanRisk(
    objective_function=ObjectiveFunction.MAXIMIZE_UTILITY,
    transaction_costs=transaction_costs,
    previous_weights=np.ones(3) / 3,
)
model_tc2.fit(X)
model_tc2.weights_
```

```none
array([0.33333336, 0.3333332 , 0.33333345])
```

Notice that the weight of General Electric becomes non-negligible due to the cost of
rebalancing the position:

```Python
model_tc2.weights_ - model.weights_
```

```none
array([-0.28439988,  0.3333332 , -0.04893332])
```

<a id="multi-period-portfolio"></a>

## 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 TC using Walk Forward Analysis:

```Python
holding_period = 60
fitting_period = 60
cv = WalkForward(train_size=fitting_period, test_size=holding_period)
```

As explained above, we transform the fix TC into a daily cost by dividing the TC by
the expected investment duration:

```Python
transaction_costs = np.array([0.01, 0.005, 0.002]) / holding_period
```

First, we train and test the model without TC:

```Python
model = MeanRisk(objective_function=ObjectiveFunction.MAXIMIZE_UTILITY)
# pred1 is a MultiPeriodPortfolio
pred1 = cross_val_predict(model, X, cv=cv, n_jobs=-1)
pred1.name = "pred1"
```

Then, we train the model without TC and test it with TC. The model trained without TC
is the same as above so we can retrieve the results and simply update the prediction
with the TC:

```Python
pred2 = MultiPeriodPortfolio(name="pred2")
previous_weights = None
for portfolio in pred1:
    new_portfolio = Portfolio(
        X=portfolio.X,
        weights=portfolio.weights,
        previous_weights=previous_weights,
        transaction_costs=transaction_costs,
    )
    previous_weights = portfolio.weights
    pred2.append(new_portfolio)
```

Finally, we train and test the model with TC.
`cross_val_predict` automatically handles the `previous_weights` dependency
between consecutive folds by propagating weights from one fold to the next.

```Python
model.set_params(transaction_costs=transaction_costs)
pred3 = cross_val_predict(model, X, cv=cv)
pred3.name = "pred3"
```

We visualize the results by plotting the cumulative returns of the successive test
periods:

```Python
population = Population([pred1, pred2, pred3])
fig = population.plot_cumulative_returns()
show(fig)
```

<!doctype html>
[plotly figure stripped from llms output]

<br/>

If we exclude the unrealistic prediction without TC, we notice that the model
**fitted with TC** outperforms the model **fitted without TC**.

**Total running time of the script:** (0 minutes 9.571 seconds)

<a id="sphx-glr-download-auto-examples-mean-risk-plot-6-transaction-costs-py"></a>
