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])

Antworten auf die Frage(2)

Ihre Antwort auf die Frage