This post is an illustrative example on how to use PortEngine, our REST API, to compare your portfolio with a hypothetical portfolio with same stocks, but with different allocations reflecting equal risk contributions (ERC). ERC, also called risk parity, is a portfolio whereby all positions have the same marginal contribution to total risk. This approach has experienced phenomenal growth and has been advocated by important investors, most notably Bridgewater and Panagora. See interesting article here.

The second step of this example involves comparing the expected shortfall (CVaR-) for both portfolios, for a 5% shock in the SP500 and dissecting the contribution from value and growth factors to that shortfall risk. This is not a trivial calculation as we want to project the portfolio’s left tail risk to a factor model, conditional on a specific shock. We will show how this can be easily achieved using PortEngine.

Why: Comparing how both portfolios react to the same shock (in this case SP down 5%) will provide an apples-to-apples reading of how much equities risk we are assuming by deviating from ERC allocations. We will also show the contributions from value and growth factors to the left tail risk changes.

Steps: a) Given a portfolio, find the equivalent ERC allocations using Scipy’s optimization module; b) Compute the difference in left tail risk from a SP 5% shock between the original portfolio and the ERC portfolio.

The simple code below will perform all calculations:

def main():
# read tickers from yahoo - retain last entry
tickerlist = {
'id1': ['IBM', 85, ''],
'id2': ['AAPL', 130, ''],
'id3': ['CVX', 100, ''],
'id4': ['GS', 50, ''],
'id5': ['F', 150, ''],
'id6': ['MMM', 30, '']
}
tickers = []
shares = []
for keys, values in tickerlist.iteritems():
tickers.append(values[0])
shares.append(values[1])
cov, data = buildCov(tickerlist)
# find risk parity weights
wtsERC = solveERC(cov.values)
# convert weights in shares
origNLV = np.sum(np.array(shares) * data[-1:].values)
sharesERC = wtsERC * origNLV / data[-1:].values
# assemble a new tickerlist with ERC shares
idx = 0
erc_tickerlist = {}
trades = {}
for keys, values in tickerlist.iteritems():
erc_tickerlist[keys] = (values[0], sharesERC[0][idx], '')
trades[values[0]] = sharesERC[0][idx] - values[1]
idx += 1
# call PortEngine
leftTail, delta_leftTail = compare_ST(tickerlist, erc_tickerlist)
print trades
print delta_leftTail
return

The first function called, **buildCov**, simply builds a covariance matrix using Adjusted closing prices for the last 2 years. We use functions from Pandas library:

def buildCov(tickerlist):
start = datetime.datetime(2015, 1, 1)
end = datetime.datetime(2016, 12, 20)
tickers = [t[0] for t in tickerlist.values()]
data = web.DataReader(tickers, 'yahoo', start, end)['Adj Close']
data = data[tickers]
ret = np.log(data / data.shift(1))[1:]
cov = ret.cov()
return (cov, data)

The next function takes a covariance matrix and computes the weights reflecting equal risk contribution, i.e. each position has the same marginal contribution to total risk:

def solveERC(S):
J, K = S.shape
initW = np.ones((J, 1)) / J
bnds = ((0, 1),) * J
cst = prepare_constraint(J)
res = minimize(riskParity, initW, constraints={'type': 'eq', 'fun': cst}, bounds=bnds, args=(S,), method='SLSQP', tol=1e-6, options={'disp': True})
return np.array(res['x'])

The function above makes a call to Scipy’s optimization module. The objective function, **riskParity** is defined below:

def riskParity(wts, S):
N, M = S.shape
portRisk = np.dot(wts, np.dot(wts, S))
mctr = wts * np.dot(S, wts)
mctr = mctr / portRisk
accum = 0.0
for i in xrange(0, N):
for j in xrange(i + 1, N):
delta_mctr = mctr[i] - mctr[j]
accum += delta_mctr * delta_mctr
return 2.0 * accum

By minimizing this objective function we are minimizing the differences on marginal contribution to total risk from each position.

The following function establishes that weights should add to 100% (Caveat: this illustrative code will work with long only portfolios):

def prepare_constraint(J):
ones = np.ones((1,J))
constraint = lambda x: 1.0 - np.dot(ones, x)
return constraint

Finally we can call **PortEngine** with 2 tickerlists, the original and the new one reflecting ERC:

def compare_ST(tickerlist, erc_tickerlist):
api_method = 'st_projection'
# make a projection list with value and growth indices
projection_list = ['IND:RLV', 'IND:RLG']
args = {
'date': '20161220',
'shock': 'IND:SPX',
'magnitude': -0.05,
'projection': projection_list
}
args['tickerlist'] = tickerlist
req = api.APIRequestor(cfg.API_SID, cfg.API_TOKEN, cfg.API_ENTRY, cfg.API_VERSION)
ret = req.request(api_method, args)
if ret['status'] != 200:
raise api.APIError(ret)
# Pre-processing
projections = ret['response']['values']
cvarneg = [i['cvarneg'] * 100.0 for i in projections]
cvarpos = [i['cvarpos'] * 100.0 for i in projections]
ev = [i['ev'] * 100.0 for i in projections]
factor_ids = [i['id'] for i in projections]
# Portfolio CVaR-, EV and CVaR+ for the specific shock:'
port_CVaRNeg = fsum(cvarneg)
port_EV = fsum(ev)
port_CVaRPos = fsum(cvarpos)
# Contribution to portfolio EV, CVaR from Industrial and Energy sectors (%):'
contrib_CVaRNeg = [(factor_ids[x], cvarneg[x]) for x in xrange(0, len(ev))]
#####################################
# Second call with erc tickerlist
#####################################
args['tickerlist'] = erc_tickerlist
ret = req.request(api_method, args)
if ret['status'] != 200:
raise api.APIError(ret)
# Pre-processing
projections = ret['response']['values']
erc_cvarneg = [i['cvarneg'] * 100.0 for i in projections]
factor_ids = [i['id'] for i in projections]
# Portfolio CVaR-, EV and CVaR+ for the specific shock:'
erc_port_CVaRNeg = fsum(erc_cvarneg)
# Contribution to portfolio EV, CVaR from Industrial and Energy sectors (%):'
delta_CVaRNeg = [(factor_ids[x], (cvarneg[x] - erc_cvarneg[x])) for x in xrange(0, len(ev))]
leftTail = (port_CVaRNeg, erc_port_CVaRNeg)
return (leftTail, delta_CVaRNeg)

The first few lines from the code above specify an on-the-fly factor model with value and growth indices (residual will be computed by PortEngine):

api_method = 'st_projection'
# make a projection list with value and growth indices
projection_list = ['IND:RLV', 'IND:RLG']

Then we proceed to compute the expected shortfall (CVaR-) for the original portfolio as well as the ERC allocations. The code returns all the deltas to show which portfolio is more vulnerable to an US equities shock. Granular details about the composition of the deltas in terms of value/growth explanatory power is also returned.

All it takes is a few lines of code. For more information on our API look at: www.everysk.com/api or send us a note to api@everysk.com to request your API key. Refer to the full code for this post: diff_2_erc.py