SciPy-Optimierung mit gruppierten Grenzen
Ich versuche, eine Portfoliooptimierung durchzuführen, die die Gewichte zurückgibt, die meine Nutzenfunktion maximieren. Ich kann diesen Teil ganz gut machen, einschließlich der Einschränkung, dass die Gewichte eins ergeben und dass die Gewichte mir auch ein Zielrisiko geben. Ich habe auch Grenzen für [0 <= Gewichte <= 1] eingefügt. Dieser Code sieht folgendermaßen aus:
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)
Jetzt kann ich mich mit dem Problem befassen. Ich habe meine Gewichte in einer MultIndex Pandas-Serie gespeichert, sodass jedes Asset ein ETF ist, der einer Assetklasse entspricht. Wenn ein Portfolio mit gleicher Gewichtung ausgedruckt wird, sieht das so aus:
Aus [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
Wie kann ich eine zusätzliche Einschränkungsanforderung einfügen, sodass beim Zusammenfassen dieser Daten die Summe der Gewichtung zwischen den Zuordnungsbereichen liegt, die ich für diese Assetklasse festgelegt habe?
Ich möchte also konkret eine zusätzliche Grenze einfügen, so dass
init_weights.groupby(level=0, axis=0).sum()
Aus [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
liegt innerhalb dieser Grenzen
[(.08,.51), (.05,.21), (.05,.41), (.05,.41), (.2,.66), (0,.16), (0,.76), (0,.11)]
[UPDATE] Ich dachte, ich würde meinen Fortschritt mit einer plumpen Pseudolösung zeigen, mit der ich nicht allzu glücklich bin. Denn es werden nicht die Gewichte über den gesamten Datensatz gelöst, sondern die Assetklassen nach Assetklassen. Das andere Problem ist, dass stattdessen die Reihe und nicht die Gewichte zurückgegeben werden. Aber ich bin mir sicher, dass jemand, der besser als ich ist, einen Einblick in die Gruppenfunktion geben könnte.
Mit einer kleinen Änderung an meinem ursprünglichen Code habe ich also:
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
Der folgende Einzeiler gibt die etwas optimierte Serie zurück
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()
[Update 2]
Die folgenden Änderungen geben die eingeschränkten Gewichte zurück, obwohl sie immer noch nicht optimal sind, da sie für die einzelnen Anlageklassen aufgeschlüsselt und optimiert sind. Wenn also die Einschränkung für das Zielrisiko berücksichtigt wird, ist nur eine reduzierte Version der anfänglichen Kovarianzmatrix verfügbar
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])