Portfolio Optimization

This notebook compares the results of several different optimization schemes.

[1]:
import os
from itertools import chain
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import numpy.linalg as la
import pandas as pd

import yabte.utilities.pandas_extension
from yabte.backtest import (
    Book,
    OrderSizeType,
    PositionalBasketOrder,
    Strategy,
    StrategyRunner,
)
from yabte.utilities.portopt.hierarchical_risk_parity import hrp
from yabte.utilities.portopt.inverse_volatility import inverse_volatility
from yabte.utilities.portopt.minimum_variance import minimum_variance
[2]:
# load sample nasdaq data
from yabte.tests._helpers import generate_nasdaq_dataset

assets, df_combined = generate_nasdaq_dataset()
[3]:
# run strategy for each portfolio optimization scheme


class PortfolioOptimizationStrat(Strategy):
    def init(self):
        data = self.data
        self.symbols = data.columns.levels[0].tolist()
        p = self.params

        # determine weekday holidays
        hols_wd = pd.date_range(data.index[0], data.index[-1], freq="B").difference(
            data.index
        )
        bday_freq = pd.tseries.offsets.CustomBusinessDay(holidays=hols_wd)

        # determine calibration days
        data.loc[:, ("_META", "CalibrationDate")] = data.index.isin(
            pd.date_range(
                sr.data.index[0], sr.data.index[-1], freq=p.calibration_frequency
            )
        )

    def on_close(self):
        # warm up - allow approx 1y to pass
        if (self.ts - self.data.index[0]).days < 252:
            return

        p = self.params

        # do calibration if we fall on correct date
        cd = self.data["_META"].CalibrationDate.iloc[-1]
        p["weights"] = None
        if cd:
            closes = self.data.loc[:, (slice(None), "Close")].droplevel(axis=1, level=1)
            returns = closes.prc.log_returns
            Sigma = returns.cov()
            R = returns.corr()
            sigma = returns.std()
            mu = closes.prc.capm_returns()

            match p.weight_scheme:
                case "HRP":
                    w = hrp(R, sigma)
                case "GMVP":
                    w = minimum_variance(Sigma, mu, p.target_return)
                    assert np.isclose(w @ mu, p.target_return)
                case "IVP":
                    w = inverse_volatility(Sigma)
                case _:
                    raise AttributeError("Unknown weight scheme")

            assert np.isclose(w.sum(), 1)
            p["weights"] = (100 * w).tolist()

        if p["weights"] is not None:
            self.orders.append(
                PositionalBasketOrder(
                    asset_names=self.symbols,
                    weights=p.weights,
                    size=1,
                    size_type=OrderSizeType.BOOK_PERCENT,
                )
            )


book = Book(name="Main", cash="100000")
sr = StrategyRunner(
    data=df_combined,
    assets=assets,
    strategies=[PortfolioOptimizationStrat()],
    books=[book],
)

srrs = []
schemes = ["HRP", "GMVP", "IVP"]
for scheme in schemes:
    srrs.append(
        sr.run(
            {
                "target_return": 0.01,  # only used for GMVP
                "weight_scheme": scheme,
                "calibration_frequency": "W-MON",
            }
        )
    )
[4]:
# compare results against market

df = pd.DataFrame(
    {scheme: srrs[ix].book_history.Main.total for ix, scheme in enumerate(schemes)}
)
first_date = df[df == 100000].last_valid_index()
market = 100000 * pd.concat(
    [
        pd.Series({first_date: 1}),
        (
            [1]
            + df_combined.loc[:, (slice(None), "Close")]
            .mean(axis=1)
            .to_frame()
            .prc.log_returns.loc[first_date:, :]
            .iloc[1:, :]
        ).cumprod(),
    ]
)
market.columns = ["MARKET"]
pd.concat([df.loc[first_date:, :], market], axis=1).plot(title="Total Book Value")
[4]:
<Axes: title={'center': 'Total Book Value'}>
../_images/notebooks_Portfolio_Optimization_4_1.png
[5]:
# plot the weightings for each scheme
fig, axs = plt.subplots(3, 1, figsize=(1 * 6, 3 * 4), sharex=True)
fig.suptitle("Individual Asset Weightings")
for ix, scheme in enumerate(schemes):
    srrs[ix].transaction_history.pivot_table(
        index="ts", values="quantity", columns="asset_name"
    ).cumsum().plot(ax=axs[ix], title=scheme)
fig.tight_layout()
../_images/notebooks_Portfolio_Optimization_5_0.png
[ ]: