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.
126 lines
4.0 KiB
126 lines
4.0 KiB
2 years ago
|
import json
|
||
|
import time
|
||
|
import numpy as np
|
||
|
import pandas as pd
|
||
|
|
||
|
from scipy.optimize import minimize
|
||
|
|
||
|
class MVO(object):
|
||
|
def __init__(self, data, market, ratio, role='max-sharpe'):
|
||
|
length, self.num = data.shape
|
||
|
tsize = int(length*ratio)
|
||
|
self.data_return = data.pct_change().dropna().to_numpy()
|
||
|
self.market_return = market.pct_change().dropna().to_numpy()
|
||
|
self.train[:tsize, :]
|
||
|
self.test[tsize:, :]
|
||
|
self.train_market = self.market_return[:tsize]
|
||
|
self.test_market = self.market_return[tsize:]
|
||
|
@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)*(-0.5)*stdPort**(-3)*(2*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)*(-0.5)*stdPort**(-3)*(2*cov_sor@w)
|
||
|
return g1+g2
|
||
|
@staticmethod
|
||
|
def volatility(w, ret):
|
||
|
retPort = ret@w # T-dimensional array
|
||
|
stdPort = np.std(retPort)
|
||
|
return stdPort
|
||
|
@staticmethod
|
||
|
def volatility_grad(w, ret, cov):
|
||
|
retPort = ret@w # T-dimensional array
|
||
|
stdPort = np.std(retPort)
|
||
|
return cov@w/stdPort**(0.5)
|
||
|
@classmethod
|
||
|
def opt(cls, ret, 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)
|
||
|
else:
|
||
|
return init
|
||
|
bnds = [[0, 0.6] for i in range(n)]
|
||
|
opts = {'maxiter': 10000, '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)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|