Portfolio AllocationΒΆ

In this quick tutorial, the portfolio allocation problem shall be investigated. Of course, this is not financial advice in any way but should illustrate how multi-objective optimization can be applied to a quite interesting problem.

Let us start by loading some data for illustration purposes. Feel free to use your own.

[1]:
import pandas as pd
import numpy as np
from pymoo.util.remote import Remote

file = Remote.get_instance().load("examples", "portfolio_allocation.csv", to=None)
df = pd.read_csv(file, parse_dates=True, index_col="date")

This tutorial is based on the Markowitz Mean-Variance Portfolio Theory and thus, we need to calculate the mean returns and covariances:

Info

Note that the problem in this case study can be solved directly using a quadratic solver (which will be much more efficient). However, such a solver finds only a single solution and must run multiple times to approximate the Pareto-optimal front. Moreover, it is worth noting that if we slightly change the problem to cubic or non-polynomial, it can not be applied anymore. The method shown provides more flexibility, for instance, optimizing objectives derived from Monte-Carlo sampling.

[2]:
returns = df.pct_change().dropna(how="all")
mu = (1 + returns).prod() ** (252 / returns.count()) - 1
cov = returns.cov() * 252

mu, cov = mu.to_numpy(), cov.to_numpy()

labels = df.columns

import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 5))
k = np.arange(len(mu))
ax.bar(k, mu)
ax.set_xticks(k, labels, rotation = 90)
plt.show()


f = plt.figure(figsize=(10, 10))
plt.matshow(returns.corr(), fignum=f.number)
plt.xticks(k, labels, fontsize=12, rotation=90)
plt.yticks(k, labels, fontsize=12)
cb = plt.colorbar()
cb.ax.tick_params(labelsize=14)
plt.title('Correlation Matrix', fontsize=16)
print("DONE")
../_images/case_studies_portfolio_allocation_7_0.png
DONE
../_images/case_studies_portfolio_allocation_7_2.png

Then let us define an optimization problem based on the theory mentioned above:

[3]:
from pymoo.core.problem import ElementwiseProblem

class PortfolioProblem(ElementwiseProblem):

    def __init__(self, mu, cov, risk_free_rate=0.02, **kwargs):
        super().__init__(n_var=len(df.columns), n_obj=2, xl=0.0, xu=1.0, **kwargs)
        self.mu = mu
        self.cov = cov
        self.risk_free_rate = risk_free_rate

    def _evaluate(self, x, out, *args, **kwargs):
        exp_return = x @ self.mu
        exp_risk = np.sqrt(x.T @ self.cov @ x)
        sharpe = (exp_return - self.risk_free_rate) / exp_risk

        out["F"] = [exp_risk, -exp_return]
        out["sharpe"] = sharpe

Now, we should consider one more fact. The variable x defines what percentage we will invest in what product. Thus, it can not be more than 100% in total. Moreover, an investment of a very small fraction does not really make sense. Thus we also incorporate each weight to be at least 1e-3 of the overall investment.

To ensure both, we can use a Repair operator (also see here) which will directly be used by the optimization method.

[4]:
from pymoo.core.repair import Repair

class PortfolioRepair(Repair):

    def _do(self, problem, X, **kwargs):
        X[X < 1e-3] = 0
        return X / X.sum(axis=1, keepdims=True)

Now let us see what solutions are found to be optimal:

[5]:
from pymoo.algorithms.moo.sms import SMSEMOA
from pymoo.optimize import minimize

problem = PortfolioProblem(mu, cov)

algorithm = SMSEMOA(repair=PortfolioRepair())

res = minimize(problem,
               algorithm,
               seed=1,
               verbose=False)

Compiled modules for significant speedup can not be used!
https://pymoo.org/installation.html#installation

To disable this warning:
from pymoo.config import Config
Config.warnings['not_compiled'] = False

The algorithm has obtained a Pareto-optimal set trading off the mean return and volatility of the portfolio.

[6]:
X, F, sharpe = res.opt.get("X", "F", "sharpe")
F = F * [1, -1]
max_sharpe = sharpe.argmax()

plt.scatter(F[:, 0], F[:, 1], facecolor="none", edgecolors="blue", alpha=0.5, label="Pareto-Optimal Portfolio")
plt.scatter(cov.diagonal() ** 0.5, mu, facecolor="none", edgecolors="black", s=30, label="Asset")
plt.scatter(F[max_sharpe, 0], F[max_sharpe, 1], marker="x", s=100, color="red", label="Max Sharpe Portfolio")
plt.legend()
plt.xlabel("expected volatility")
plt.ylabel("expected return")
plt.show()
../_images/case_studies_portfolio_allocation_15_0.png

A common way for the decision making is looking at the sharpe ratio shown below:

[7]:
import operator

allocation = {name: w for name, w in zip(df.columns, X[max_sharpe])}
allocation = sorted(allocation.items(), key=operator.itemgetter(1), reverse=True)

print("Allocation With Best Sharpe")
for name, w in allocation:
    print(f"{name:<5} {w}")
Allocation With Best Sharpe
MA    0.364421440541109
FB    0.20874148720289187
PFE   0.19574628824693652
AAPL  0.0769256140818164
BABA  0.07129945833640815
AMZN  0.04478765737146619
GOOG  0.03068450348113022
BBY   0.00739355073824167
GE    0.0
AMD   0.0
WMT   0.0
BAC   0.0
GM    0.0
T     0.0
UAA   0.0
SHLD  0.0
XOM   0.0
RRC   0.0
JPM   0.0
SBUX  0.0