#!/usr/bin/env python3.11
# Copyright 2024, Gurobi Optimization, LLC
# Portfolio selection: given a sum of money to invest, one must decide how to
# spend it amongst a portfolio of financial securities. Our approach is due
# to Markowitz (1959) and looks to minimize the risk associated with the
# investment while realizing a target expected return. By varying the target,
# one can compute an 'efficient frontier', which defines the optimal portfolio
# for a given expected return.
#
# Note that this example reads historical return data from a comma-separated
# file (../data/portfolio.csv). As a result, it must be run from the Gurobi
# examples/python directory.
#
# This example requires the pandas (>= 0.20.3), NumPy, and Matplotlib
# Python packages, which are part of the SciPy ecosystem for
# mathematics, science, and engineering (http://scipy.org). These
# packages aren't included in all Python distributions, but are
# included by default with Anaconda Python.
import gurobipy as gp
from gurobipy import GRB
from math import sqrt
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# Import (normalized) historical return data using pandas
data = pd.read_csv("../data/portfolio.csv", index_col=0)
stocks = data.columns
# Calculate basic summary statistics for individual stocks
stock_volatility = data.std()
stock_return = data.mean()
# Turn off all logging and create an empty model
with gp.Env(params={"OutputFlag": 0}) as env, gp.Model("portfolio", env=env) as m:
# Add a variable for each stock
vars = pd.Series(m.addVars(stocks), index=stocks)
# Objective is to minimize risk (squared). This is modeled using the
# covariance matrix, which measures the historical correlation between stocks.
sigma = data.cov()
portfolio_risk = sigma.dot(vars).dot(vars)
m.setObjective(portfolio_risk, GRB.MINIMIZE)
# Fix budget with a constraint
m.addConstr(vars.sum() == 1, "budget")
# Optimize model to find the minimum risk portfolio
m.optimize()
# Create an expression representing the expected return for the portfolio
portfolio_return = stock_return.dot(vars)
# Display minimum risk portfolio
print("Minimum Risk Portfolio:\n")
for v in vars:
if v.X > 0:
print(f"\t{v.VarName}\t: {v.X:g}")
minrisk_volatility = sqrt(portfolio_risk.getValue())
print(f"\nVolatility = {minrisk_volatility:g}")
minrisk_return = portfolio_return.getValue()
print(f"Expected Return = {minrisk_return:g}")
# Add (redundant) target return constraint
target = m.addConstr(portfolio_return == minrisk_return, "target")
# Solve for efficient frontier by varying target return
frontier = pd.Series(dtype=np.float64)
for r in np.linspace(stock_return.min(), stock_return.max(), 100):
target.rhs = r
m.optimize()
frontier.loc[sqrt(portfolio_risk.getValue())] = r
# Plot volatility versus expected return for individual stocks
ax = plt.gca()
ax.scatter(x=stock_volatility, y=stock_return, color="Blue", label="Individual Stocks")
for stock in stocks:
ax.annotate(stock, (stock_volatility[stock], stock_return[stock]))
# Plot volatility versus expected return for minimum risk portfolio
ax.scatter(x=minrisk_volatility, y=minrisk_return, color="DarkGreen")
ax.annotate(
"Minimum\nRisk\nPortfolio",
(minrisk_volatility, minrisk_return),
horizontalalignment="right",
)
# Plot efficient frontier
frontier.plot(color="DarkGreen", label="Efficient Frontier", ax=ax)
# Format and display the final plot
ax.axis((0.005, 0.06, -0.02, 0.025))
ax.set_xlabel("Volatility (standard deviation)")
ax.set_ylabel("Expected Return")
ax.legend()
ax.grid()
plt.savefig("portfolio.png")
print("Plotted efficient frontier to 'portfolio.png'")