Warning
This page was created from a pull request (#70).
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.
Adapted from the Gurobi examples. This does not illustrate the accessor API since all constraints are single expressions.
Point to think about though: when we do construct a single expression by applying pandas operations to series, the user has to fall back onto gurobipy methods.
[1]:
import gurobipy as gp
from gurobipy import GRB
from math import sqrt
import gurobipy_pandas as gppd
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
Import (normalized) historical return data using pandas
[2]:
data = pd.read_csv('data/portfolio.csv', index_col=0)
Create a new model and add a variable for each stock. The columns in our dataframe correspond to stocks, so the columns can be used directly (as a pandas index) to construct the necessary variable.
[3]:
model = gp.Model('Portfolio')
stocks = gppd.add_vars(model, data.columns, name="Stock")
model.update()
stocks
Restricted license - for non-production use only - expires 2025-11-24
[3]:
AA <gurobi.Var Stock[AA]>
BB <gurobi.Var Stock[BB]>
CC <gurobi.Var Stock[CC]>
DD <gurobi.Var Stock[DD]>
EE <gurobi.Var Stock[EE]>
FF <gurobi.Var Stock[FF]>
GG <gurobi.Var Stock[GG]>
HH <gurobi.Var Stock[HH]>
II <gurobi.Var Stock[II]>
JJ <gurobi.Var Stock[JJ]>
Name: Stock, dtype: object
Objective is to minimize risk (squared). This is modeled using the covariance matrix, which measures the historical correlation between stocks.
[4]:
sigma = data.cov()
portfolio_risk = sigma.dot(stocks).dot(stocks)
model.setObjective(portfolio_risk, GRB.MINIMIZE)
Fix budget with a constraint. For summation over series, we get back just a single expression, so this constraint is added directly to the model (not through the accessors).
[5]:
model.addConstr(stocks.sum() == 1, name='budget');
Optimize model to find the minimum risk portfolio.
[6]:
model.optimize()
Gurobi Optimizer version 11.0.0 build v11.0.0rc2 (linux64 - "Ubuntu 20.04.6 LTS")
CPU model: Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads
Optimize a model with 1 rows, 10 columns and 10 nonzeros
Model fingerprint: 0x9181cbde
Model has 55 quadratic objective terms
Coefficient statistics:
Matrix range [1e+00, 1e+00]
Objective range [0e+00, 0e+00]
QObjective range [3e-05, 7e-03]
Bounds range [0e+00, 0e+00]
RHS range [1e+00, 1e+00]
Presolve time: 0.01s
Presolved: 1 rows, 10 columns, 10 nonzeros
Presolved model has 55 quadratic objective terms
Ordering time: 0.00s
Barrier statistics:
Free vars : 9
AA' NZ : 4.500e+01
Factor NZ : 5.500e+01
Factor Ops : 3.850e+02 (less than 1 second per iteration)
Threads : 1
Objective Residual
Iter Primal Dual Primal Dual Compl Time
0 4.16767507e+03 -4.16767507e+03 1.00e+04 8.08e-06 1.00e+06 0s
1 4.45599962e-03 -7.80471122e+00 1.09e+01 8.83e-09 1.14e+03 0s
2 4.17460816e-04 -7.14114851e+00 1.09e-05 8.84e-15 4.57e+01 0s
3 4.17365279e-04 -7.33725047e-03 9.40e-10 2.78e-17 4.96e-02 0s
4 3.50058972e-04 -1.09469258e-04 2.26e-11 1.39e-17 2.94e-03 0s
5 1.95819453e-04 5.77538068e-05 2.22e-16 2.69e-17 8.84e-04 0s
6 1.72155801e-04 1.57430436e-04 5.55e-17 6.94e-18 9.42e-05 0s
7 1.66577780e-04 1.65061129e-04 6.66e-16 1.57e-17 9.71e-06 0s
8 1.65913497e-04 1.65828685e-04 1.67e-16 1.39e-17 5.43e-07 0s
9 1.65877870e-04 1.65873375e-04 5.41e-16 1.85e-18 2.88e-08 0s
10 1.65877097e-04 1.65877052e-04 3.39e-15 1.39e-17 2.88e-10 0s
Barrier solved model in 10 iterations and 0.03 seconds (0.00 work units)
Optimal objective 1.65877097e-04
Display the minimum risk portfolio.
[7]:
stocks.gppd.X.round(3)
[7]:
AA 0.468
BB 0.000
CC 0.003
DD 0.125
EE 0.000
FF 0.033
GG 0.017
HH 0.205
II 0.149
JJ 0.000
Name: Stock, dtype: float64
Key metrics
[8]:
minrisk_volatility = sqrt(portfolio_risk.getValue())
print('\nVolatility = %g' % minrisk_volatility)
stock_return = data.mean()
portfolio_return = stock_return.dot(stocks)
minrisk_return = portfolio_return.getValue()
print('Expected Return = %g' % minrisk_return)
Volatility = 0.0128793
Expected Return = 0.00243537
Solve for the efficient frontier by varying the target return (sampling).
One useful point here could be the ability to specify scenarios by mapping onto constraints
[9]:
scenarios = pd.DataFrame(dict(target_return=np.linspace(stock_return.min(), stock_return.max())))
target_return = model.addConstr(portfolio_return == minrisk_return, 'target_return')
model.update()
model.NumScenarios = len(scenarios)
for row in scenarios.itertuples():
model.Params.ScenarioNumber = row.Index
target_return.ScenNRHS = row.target_return
model.Params.OutputFlag = 0
model.optimize()
results = []
for row in scenarios.itertuples():
model.Params.ScenarioNumber = row.Index
results.append(model.ScenNObjVal)
scenarios = scenarios.assign(ObjVal=pd.Series(results, index=scenarios.index))
scenarios['volatility'] = scenarios['ObjVal'].apply(sqrt)
scenarios.head()
[9]:
| target_return | ObjVal | volatility | |
|---|---|---|---|
| 0 | -0.013629 | 0.002303 | 0.047985 |
| 1 | -0.013061 | 0.002114 | 0.045981 |
| 2 | -0.012492 | 0.001937 | 0.044008 |
| 3 | -0.011924 | 0.001769 | 0.042059 |
| 4 | -0.011356 | 0.001611 | 0.040140 |
[10]:
# Plot volatility versus expected return for individual stocks
stock_volatility = data.std()
ax = plt.gca()
ax.scatter(x=stock_volatility, y=stock_return,
color='Blue', label='Individual Stocks')
for i, stock in enumerate(stocks.index):
ax.annotate(stock, (stock_volatility[i], stock_return[i]))
# 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
scenarios.plot.line(x='volatility', y='target_return', ax=ax, color='DarkGreen')
# 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()