SciPy优化和分组边界

6

我正在尝试进行投资组合优化,以返回最大化我的效用函数的权重。我可以很好地完成这部分工作,包括约束权重总和为1并且权重还给我一个目标风险。我还为[0 <= 权重 <= 1]设置了边界。这段代码如下:

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)

现在我可以深入研究这个问题了。我将我的权重存储在一个MultIndex pandas Series中,以便每个资产都是对应于某个资产类别的ETF。当等权重组合被打印出来时,它看起来像这样:
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
如何包含一个额外的限制要求,在将此数据分组时,权重总和落在我预先确定的该资产类别的分配范围之间?
具体来说,我想包含一个额外的边界,使得:
init_weights.groupby(level=0, axis=0).sum()
股票         0.136364
国际股票     0.136364
债券         0.136364
国际债券     0.136364
商品         0.136364
市盈率      0.136364
对冲基金   0.090909
现金         0.090909
dtype: float64

在这些范围内

[(.08,.51), (.05,.21), (.05,.41), (.05,.41), (.2,.66), (0,.16), (0,.76), (0,.11)]

[更新] 我想展示一下我的进展,这是一个笨拙的伪解决方案,我对它并不满意。主要是因为它没有使用整个数据集来解决权重问题,而是按资产类别进行了处理。另一个问题是它返回了系列而不是权重,但我相信比我更能胜任的人可以就groupby函数提供一些见解。
所以通过对我最初的代码进行轻微调整,我有了以下结果:
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

以下一行代码将返回较为优化的系列。
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()

enter image description here

[更新2]

以下更改将返回受限权重,虽然仍不是最优解,因为它被分解和优化为组成资产类别,因此当考虑目标风险约束时,仅可用初始协方差矩阵的折叠版本。

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

我不确定我理解问题的意思。您是想检查值是否在提供的范围内并返回布尔True/False数组,还是想用这些边界表示类别来表示您的值? - Viktor Kerkez
我正在尝试使用scipy.opt来返回符合各自绑定到(0,1)的权重,然后还要使分组类别的权重受到上述限制的约束。 - Brandon Ogle
对于第一个问题的部分:等待时间已经被限制了吗?您已经正确地向“最小化”方法提供了“bounds”参数,因此它应该返回一个有限权重列表? - Viktor Kerkez
是的,所有权重都在(0,1)之间。现在我想添加第二个边界。 - Brandon Ogle
为了举例说明,假设我在伪代码中使用约束条件:c_ = ({'type':'eq', 'fun': lambda W: sum(W) - 1}, {'type':'eq', 'fun': lambda W: .08 <= sum(equity weights) <= .51}, {'type':'eq', 'fun': lambda W: .05 <= sum(intl_equity weights) <= .21}等等...... - Brandon Ogle
这与你的问题无关(真的完全无关),但通常不建议引发BaseException甚至是Exception,最好是使用更具信息性的名称对其进行子类化或使用内置函数。在您的情况下,您可能可以使用ValueError - Phillip Cloud
2个回答

3

我不是完全确定是否理解了,但我认为您可以将以下内容作为另一个约束条件添加:

def w_opt(W):
    def filterer(x):
        v = x.range.values
        tp = v[0]
        lower, upper = tp
        return lower <= x[column_name].sum() <= upper
    return not W.groupby(level=0, axis=0).filter(filterer).empty

c_ = {'type': 'eq', 'fun': w_opt}  # add this to your other constraints

其中x.range是一个区间(tuple),重复出现K[i]次,其中K是特定级别发生的次数,i是第i个级别。在您的情况下,column_name恰好是一个日期。

这意味着约束权重,使得第i组中权重的总和介于相关的tuple区间之间。

要将每个级别名称映射到一个区间,请执行以下操作:

intervals = [(.08,.51), (.05,.21), (.05,.41), (.05,.41), (.2,.66), (0,.16), (0,.76), (0,.11)]
names = ['equity', 'intl_equity', 'bond', 'intl_bond', 'commodity', 'pe', 'hf', 'cash']

mapper = Series(zip(names, intervals))
fully_mapped = mapper[init_weights.get_level_values(0)]
original_dataset['range'] = fully_mapped.values

GroupBy对象上的filter方法是由提交2a2cfb83582ece7690c94457cc5a6f0835049f5c引入的,要使用它,您需要升级到pandas 0.12。我会看看是否可以找到一个等效的解决方案,而不使用filter - Phillip Cloud
但是如果我正确理解你的代码,它会测试分组级别是否在我的范围内,这正是我想要的。然而,我不熟悉“filter”,也不确定我应该在哪里传递我的范围?因为你没有在这里传递它 W.groupby(level=0, axis=0).filter(filterer).empty - Brandon Ogle
将我在solve_weights中定义的字典c_添加到您在上面定义的dict元组中。 - Phillip Cloud
哦,我明白了问题所在,我误解了你上次的评论。我的做法是假设你有一个名为“range”的元组列,其中包含该特定组的区间。或者,更好的方法是有两列,比如说,“lower”和“upper”,它们分别是你的区间的下限和上限点。 - Phillip Cloud
抱歉,我应该少说一点。在将新的等式函数添加到c_之后,我在哪里传递这些[(.08,.51), (.05,.21), (.05,.41), (.05,.41), (.2,.66), (0,.16), (0,.76), (0,.11)]。或者你的函数只是从命名空间中获取它们?我将阅读过滤方法并进行更新。 - Brandon Ogle
显示剩余5条评论

2

经过很长时间的探索,这似乎是唯一适合的解决方案...

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)

class_cont.groupby(level=0).transform(lambda x: x * scalars[x.name])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接