Optymalizacja SciPy z pogrupowanymi granicami
Próbuję wykonać optymalizację portfela, która zwraca wagi, które maksymalizują moją funkcję użyteczności. Mogę wykonać tę część w porządku, w tym ograniczenie, które waży sumę do jednego i że wagi również dają mi ryzyko docelowe. Zawarłem również granice dla [0 <= wagi <= 1]. Ten kod wygląda następująco:
def rebalance(PortValue, port_rets, risk_tgt):
#convert continuously compounded returns to simple returns
Rt = np.exp(port_rets) - 1
covar = Rt.cov()
def fitness(W):
port_Rt = np.dot(Rt, W)
port_rt = np.log(1 + port_Rt)
q95 = Series(port_rt).quantile(.05)
cVaR = (port_rt[port_rt < q95] * sqrt(20)).mean() * PortValue
mean_cVaR = (PortValue * (port_rt.mean() * 20)) / cVaR
return -1 * mean_cVaR
def solve_weights(W):
import scipy.optimize as opt
b_ = [(0.0, 1.0) for i in Rt.columns]
c_ = ({'type':'eq', 'fun': lambda W: sum(W) - 1},
{'type':'eq', 'fun': lambda W: sqrt(np.dot(W, np.dot(covar, W))\
* 252) - risk_tgt})
optimized = opt.minimize(fitness, W, method='SLSQP', constraints=c_, bounds=b_)
if not optimized.success:
raise BaseException(optimized.message)
return optimized.x # Return optimized weights
init_weights = Rt.ix[1].copy()
init_weights.ix[:] = np.ones(len(Rt.columns)) / len(Rt.columns)
return solve_weights(init_weights)
Teraz mogę zagłębić się w problem, mam wagi zapisane w serii pand MultIndex tak, że każdy zasób jest ETF odpowiadającym klasie aktywów. Kiedy drukowany jest portfel o jednakowej wadze, wygląda to tak:
Out [263]:equity CZA 0.045455 IWM 0.045455 SPY 0.045455 intl_equity EWA 0.045455 EWO 0.045455 IEV 0.045455 bond IEF 0.045455 SHY 0.045455 TLT 0.045455 intl_bond BWX 0.045455 BWZ 0.045455 IGOV 0.045455 commodity DBA 0.045455 DBB 0.045455 DBE 0.045455 pe ARCC 0.045455 BX 0.045455 PSP 0.045455 hf DXJ 0.045455 SRV 0.045455 cash BIL 0.045455 GSY 0.045455 Name: 2009-05-15 00:00:00, dtype: float64
W jaki sposób mogę dołączyć wymóg dodatkowych ograniczeń, tak aby podczas grupowania tych danych suma wagi mieściła się między zakresami alokacji, które z góry ustaliłem dla tej klasy aktywów?
Tak konkretnie chcę zawrzeć dodatkową granicę taką, że
init_weights.groupby(level=0, axis=0).sum()
Out [264]:equity 0.136364 intl_equity 0.136364 bond 0.136364 intl_bond 0.136364 commodity 0.136364 pe 0.136364 hf 0.090909 cash 0.090909 dtype: float64
jest w tych granicach
[(.08,.51), (.05,.21), (.05,.41), (.05,.41), (.2,.66), (0,.16), (0,.76), (0,.11)]
[AKTUALIZACJA] Pomyślałem, że pokażę swoje postępy z niezdarnym rozwiązaniem psuedo, z którego nie jestem zbyt zadowolony. Mianowicie dlatego, że nie rozwiązuje wag przy użyciu całego zestawu danych, ale raczej klasy aktywów według klasy aktywów. Inną kwestią jest to, że zamiast tego zwraca serię, a nie wagi, ale jestem pewien, że ktoś bardziej trafny niż ja sam może zaoferować pewien wgląd w funkcję groupby.
Więc z łagodnym ulepszeniem mojego początkowego kodu, mam:
PortValue = 100000
model = DataFrame(np.array([.08,.12,.05,.05,.65,0,0,.05]), index= port_idx, columns = ['strategic'])
model['tactical'] = [(.08,.51), (.05,.21),(.05,.41),(.05,.41), (.2,.66), (0,.16), (0,.76), (0,.11)]
def fitness(W, Rt):
port_Rt = np.dot(Rt, W)
port_rt = np.log(1 + port_Rt)
q95 = Series(port_rt).quantile(.05)
cVaR = (port_rt[port_rt < q95] * sqrt(20)).mean() * PortValue
mean_cVaR = (PortValue * (port_rt.mean() * 20)) / cVaR
return -1 * mean_cVaR
def solve_weights(Rt, b_= None):
import scipy.optimize as opt
if b_ is None:
b_ = [(0.0, 1.0) for i in Rt.columns]
W = np.ones(len(Rt.columns))/len(Rt.columns)
c_ = ({'type':'eq', 'fun': lambda W: sum(W) - 1})
optimized = opt.minimize(fitness, W, args=[Rt], method='SLSQP', constraints=c_, bounds=b_)
if not optimized.success:
raise ValueError(optimized.message)
return optimized.x # Return optimized weights
Następujący jeden liner zwróci nieco zoptymalizowaną serię
port = np.dot(port_rets.groupby(level=0, axis=1).agg(lambda x: np.dot(x,solve_weights(x))),\
solve_weights(port_rets.groupby(level=0, axis=1).agg(lambda x: np.dot(x,solve_weights(x))), \
list(model['tactical'].values)))
Series(port, name='portfolio').cumsum().plot()
[Aktualizacja 2]
Następujące zmiany zwrócą ograniczoną wagę, choć nadal nie są optymalne, ponieważ są podzielone i zoptymalizowane w klasach aktywów składowych, więc gdy ograniczenie dla ryzyka docelowego jest uznawane, dostępna jest tylko zwinięta wersja początkowej macierzy kowariancji
def solve_weights(Rt, b_ = None):
W = np.ones(len(Rt.columns)) / len(Rt.columns)
if b_ is None:
b_ = [(0.01, 1.0) for i in Rt.columns]
c_ = ({'type':'eq', 'fun': lambda W: sum(W) - 1})
else:
covar = Rt.cov()
c_ = ({'type':'eq', 'fun': lambda W: sum(W) - 1},
{'type':'eq', 'fun': lambda W: sqrt(np.dot(W, np.dot(covar, W)) * 252) - risk_tgt})
optimized = opt.minimize(fitness, W, args = [Rt], method='SLSQP', constraints=c_, bounds=b_)
if not optimized.success:
raise ValueError(optimized.message)
return optimized.x # Return optimized weights
class_cont = Rt.ix[0].copy()
class_cont.ix[:] = np.around(np.hstack(Rt.groupby(axis=1, level=0).apply(solve_weights).values),3)
scalars = class_cont.groupby(level=0).sum()
scalars.ix[:] = np.around(solve_weights((class_cont * port_rets).groupby(level=0, axis=1).sum(), list(model['tactical'].values)),3)
return class_cont.groupby(level=0).transform(lambda x: x * scalars[x.name])