← 返回文章列表 组合优化

用 cvxpy 做带约束的权重优化

行业暴露、单票上限、换手预算写成约束条件,比事后截断干净得多。

事后截断的问题

很多人的做法是:先按打分算权重,然后把超过 3% 的削到 3%,再归一化。这一步看起来无害,实际上它破坏了权重和信号之间的单调关系,也没有真正控制住行业暴露——削掉的部分被平摊回了同一个行业里的其他票。

把这些要求直接写成约束,让求解器在满足约束的前提下最大化预期收益,得到的组合结构完全不同。

import cvxpy as cp
import numpy as np

n = len(mu)                  # mu: 预期收益,长度 n
w = cp.Variable(n)
w0 = prev_weight             # 上期权重,用来算换手

objective = cp.Maximize(mu @ w - 0.5 * gamma * cp.quad_form(w, Sigma))

constraints = [
    cp.sum(w) == 1,                       # 满仓
    w >= 0,                               # 只做多
    w <= 0.03,                            # 单票上限 3%
    cp.abs(ind_matrix @ w - ind_bench) <= 0.02,   # 行业偏离 ±2%
    cp.norm(w - w0, 1) <= 0.30,           # 单期双边换手不超过 30%
]

prob = cp.Problem(objective, constraints)
prob.solve(solver=cp.OSQP)
print(prob.status, prob.value)

gamma 怎么定

风险厌恶系数不要拍脑袋。先固定一个目标跟踪误差,比如年化 4%,然后二分搜索 gamma 使得优化解的事前跟踪误差落在这个数附近,这样参数才有可解释的含义。

协方差矩阵才是难点

目标函数里的 Sigma 用样本协方差几乎一定会失败:3000 只股票、250 个交易日,样本协方差矩阵是奇异的,求解器会在最小特征值方向上把杠杆推到极端。

from sklearn.covariance import LedoitWolf

Sigma = LedoitWolf().fit(ret_window.values).covariance_
Sigma = cp.psd_wrap(Sigma)   # 数值上保证半正定,避免求解器报 non-convex

无解的时候怎么办

约束加多了迟早会遇到 infeasible。不要靠删约束来救,改成软约束:把违约量作为惩罚项放进目标函数,给一个足够大的系数。这样求解器总能返回一个解,而且你能从违约量上直接看出是哪条约束太紧。

slack = cp.Variable(nonneg=True)
constraints = [
    cp.norm(w - w0, 1) <= 0.30 + slack,
]
objective = cp.Maximize(mu @ w - 0.5 * gamma * cp.quad_form(w, Sigma) - 50 * slack)

下一篇讲怎么把换手约束从单期扩展到多期,也就是带前瞻的动态规划版本。

cvxpy组合优化约束
下一篇

回撤控制到底该放在信号层还是仓位层