You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

143 lines
4.6 KiB

import json
import time
import numpy as np
import pandas as pd
from scipy.optimize import minimize
class MVO(object):
@staticmethod
def portfolio_info(w, ret, market_ret, rf=0):
# return and drawdown
retPort = ret@w # T-dimensional array
cum_ret = (retPort+1).cumprod()
rolling_max=np.maximum.accumulate(cum_ret)
mdd = np.max((rolling_max - cum_ret)/rolling_max)
## Sharpe Ratio
stdPort = np.std(retPort)
vol = stdPort*15.87451
annual_ret = np.mean(retPort) * 252
annual_sr = (annual_ret-rf) / vol
## alpha, beta
cov = np.cov(retPort, market_ret)
beta = cov[0, 1] / cov[1, 1]
alpha = annual_ret - rf - beta*(np.mean(market_ret) * 252 - rf)
R2 = cov[0, 1]**2/(cov[0, 0] * cov[1, 1])
## n-day 95% VaR
var10 = -annual_ret*(10/252) + 1.645*vol*(10/252)**(1/2)
d = dict(annual_ret = annual_ret,
vol=vol,
mdd=mdd,
annual_sr=annual_sr,
beta=beta,
alpha=alpha,
var10=var10,
R2=R2)
return {key: round(d[key], 2) for key in d}
@staticmethod
def sharpe_ratio(w, ret):
cov = np.cov(ret.T)
# print(cov.shape, w.shape)
retPort = ret@w # T-dimensional array
stdPort = np.std(retPort)
return np.mean(retPort)/stdPort
@staticmethod
def sharpe_grad(w, ret, cov):
manual_ret = np.mean(ret, axis=0)
# print(cov.shape, w.shape)
retPort = ret@w # T-dimensional array
stdPort = np.std(retPort)
g1=manual_ret/stdPort
g2=np.mean(retPort)*stdPort**(-3)*cov@w
return g1-g2
@staticmethod
def sortino_ratio(w, ret):
retPort = ret@w # T-dimensional array
stdPort = np.std(np.maximum(-retPort, 0))
return np.mean(retPort)/stdPort
@staticmethod
def sortino_grad(w, ret, cov_sor):
manual_ret = np.mean(ret, axis=0)
# print(cov.shape, w.shape)
retPort = ret@w # T-dimensional arrayss
stdPort = np.std(retPort)
g1=manual_ret/stdPort
g2=np.mean(retPort)*stdPort**(-3)*cov_sor@w
return g1-g2
@staticmethod
def sortino_ratio(w, ret):
retPort = ret@w # T-dimensional array
stdPort = np.std(np.maximum(-retPort, 0))
return np.mean(retPort)/stdPort
@staticmethod
def sortino_grad(w, ret, cov_sor):
manual_ret = np.mean(ret, axis=0)
# print(cov.shape, w.shape)
retPort = ret@w # T-dimensional arrayss
stdPort = np.std(retPort)
g1=manual_ret/stdPort
g2=np.mean(retPort)*stdPort**(-3)*cov_sor@w
return g1-g2
# equivalent opt problem with min vol
@staticmethod
def volatility(w, ret):
retPort = ret@w # T-dimensional array
return np.std(retPort)
@staticmethod
def volatility_grad(w, ret, cov):
retPort = ret@w # T-dimensional array
stdPort = np.std(retPort)
return cov@w/stdPort
@staticmethod
def quadratic_utility(w, ret, gamma):
retPort = ret@w # T-dimensional array
varPort = np.var(retPort)
return np.mean(retPort) - 0.5*gamma*varPort
@staticmethod
def quadratic_utility_grad(w, ret, cov, gamma):
manual_ret = np.mean(ret, axis=0)
return manual_ret - gamma*cov@w
@classmethod
def opt(cls, ret, gamma=0, role="max_sharpe"):
n = ret.shape[1]
init=np.ones(n)/n
if role=="max_sharpe":
cov=np.cov(ret.T)
loss = lambda w: -cls.sharpe_ratio(w, ret)
grad = lambda w: -cls.sharpe_grad(w, ret, cov)
elif role=="max_sortino":
cov = np.cov(np.maximum(ret, 0).T)
loss = lambda w: -cls.sortino_ratio(w, ret)
grad = lambda w: -cls.sortino_grad(w, ret, cov)
elif role=="min_volatility":
cov=np.cov(ret.T)
loss = lambda w: cls.volatility(w, ret)
grad = lambda w: cls.volatility_grad(w, ret, cov)
elif role=="quadratic_utility":
cov=np.cov(ret.T)
loss = lambda w: -cls.quadratic_utility(w, ret, gamma)
grad = lambda w: -cls.quadratic_utility_grad(w, ret, cov, gamma)
else:
return init
bnds = [[0, 0.6] for i in range(n)]
opts = {'maxiter': 1000, 'disp': False}
cons = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
result = minimize(loss, init, method="SLSQP",\
options=opts, bounds=bnds, tol = None, jac = grad, constraints=cons)
sol = result['x']
return np.round(sol, 2)