2. CVXPY基础

在进入多策略资金配置的建模实战之前,必须先把 CVXPY 的“最小可用闭环”跑通:能装、能建变量、能写目标与约束、能选求解器、能读结果。本节围绕这五个环节展开,帮助你建立一套可复用的建模心智模型,而不是零散地记 API。

一、核心原理:CVXPY 到底在做什么

CVXPY 是一个嵌入 Python 的凸优化建模语言。它的核心思想是“建模与求解分离”:你只负责用接近数学表达式的语法描述问题,CVXPY 负责把问题转化为标准锥规划形式(如 LP、QP、SOCP、SDP),再交给底层求解器计算。因此,CVXPY 本身不是求解器,而是“翻译层 + 调度层”。

一个 CVXPY 问题由三部分组成:变量(Variable/Parameter)、目标函数(Objective)、约束(Constraints)。变量是待求的未知量;参数是已知但可复用的数据,便于做敏感性分析和滚动回测;目标函数是你要最小化或最大化的标量表达式;约束则用等式或不等式限定可行域。CVXPY 要求问题满足 DCP(Disciplined Convex Programming)规则,即目标与约束必须按凸性规则组合,否则会在建模阶段直接报错,而不是等到求解时才失败。

DCP 规则是 CVXPY 的“语法检查器”。它不保证问题一定有解,但能保证你写出的问题在数学上是凸的,从而避免陷入局部最优或不可解陷阱。

二、安装与环境配置

推荐使用 Python 3.9 及以上版本,并在虚拟环境中安装,避免与系统包冲突。CVXPY 默认会安装开源求解器 ECOS、SCS、OSQP,足以覆盖 LP、QP、SOCP 等常见问题。若需要更高性能或商用求解器,可额外安装 Gurobi、MOSEK、COPT 等。

# 创建并激活虚拟环境
python -m venv cvxpy_env
source cvxpy_env/bin/activate        # Windows 使用 cvxpy_env\Scripts\activate

# 安装 CVXPY 及常用依赖
pip install cvxpy numpy pandas matplotlib

# 可选:安装商用求解器接口
pip install gurobipy mosek coptpy

安装完成后,用一段最小代码验证环境是否正常。若输出求解状态为 optimal,说明安装成功。

import cvxpy as cp
import numpy as np

x = cp.Variable(2)
prob = cp.Problem(cp.Minimize(cp.sum_squares(x - np.array([1, 2]))),
                  [x >= 0, cp.sum(x) == 2])
prob.solve()
print("status:", prob.status)
print("x:", x.value)
print("optimal value:", prob.value)

三、变量与参数定义

变量定义要关注三个维度:形状、符号、属性。形状决定变量是标量、向量还是矩阵;符号用于声明非负、整数或布尔;属性如对称、半正定等用于结构化建模。参数则用于承载已知数据,例如资产收益率、协方差矩阵、交易成本系数等。

  • cp.Variable(n):长度为 n 的向量变量。
  • cp.Variable((m, n)):m×n 矩阵变量。
  • cp.Variable(nonneg=True):非负变量,常用于权重或持仓。
  • cp.Variable(integer=True):整数变量,用于离散手数或整份配置。
  • cp.Parameter(n, nonneg=True):参数,值可在求解前反复赋值。

在多策略资金配置中,一个典型写法是:权重变量 w 非负且和为 1,收益参数 mu 和协方差参数 Sigma 作为 Parameter 传入,便于滚动窗口回测。

n = 5
w = cp.Variable(n, nonneg=True)
mu = cp.Parameter(n)
Sigma = cp.Parameter((n, n), PSD=True)

mu.value = np.array([0.08, 0.10, 0.06, 0.12, 0.07])
Sigma.value = np.eye(n) * 0.01

四、目标函数与约束表达

目标函数必须是一个标量表达式。常见形式包括:最小化方差 cp.quad_form(w, Sigma)、最大化收益 mu @ w、最小化绝对偏差 cp.norm1(w - w_prev)、最小化最大回撤近似 cp.max(...)。约束则用列表传入,支持等式、不等式、锥约束和集合约束。

下面是一个“均值—方差 + 换手率惩罚”的典型配置模型。注意目标函数中同时包含收益、风险和交易成本,约束中限定权重和为 1、单资产上限以及换手率上限。

w_prev = np.array([0.2, 0.2, 0.2, 0.2, 0.2])
gamma = 2.0        # 风险厌恶系数
lambda_tc = 0.001  # 交易成本系数

objective = cp.Maximize(mu @ w - gamma * cp.quad_form(w, Sigma)
                        - lambda_tc * cp.norm1(w - w_prev))
constraints = [
    cp.sum(w) == 1,
    w <= 0.4,
    cp.norm1(w - w_prev) <= 0.3
]
prob = cp.Problem(objective, constraints)
常见误区:把非凸表达式直接写入目标或约束,例如 cp.maximum(w, 0) * w 或 cp.square(w) / w。CVXPY 会抛出 DCPError。正确做法是引入辅助变量或改用凸近似。

五、求解器选择与结果读取

CVXPY 会根据问题类型自动选择求解器,但显式指定求解器能获得更稳定的行为和更清晰的日志。LP 优先用 ECOS 或 GLPK;QP 用 OSQP;SOCP 用 ECOS 或 SCS;SDP 用 SCS 或 MOSEK。若问题规模大或需要高精度,可切换商用求解器。

问题类型推荐求解器适用场景备注
LPECOS / GLPK线性目标与约束速度快,开源
QPOSQP二次目标、线性约束适合组合优化
SOCPECOS / SCS范数约束、鲁棒优化SCS 更通用
SDPSCS / MOSEK半正定约束MOSEK 精度高
MIPGUROBI / CPLEX整数变量需商用授权

求解后,结果读取主要看四个字段:prob.status 表示求解状态;prob.value 表示最优目标值;variable.value 表示变量取值;prob.solver_stats 提供求解器名称、迭代次数和耗时。若状态为 infeasible 或 unbounded,应优先检查约束是否冲突或目标是否无界。

prob.solve(solver=cp.OSQP, verbose=True)
print("status:", prob.status)
print("optimal value:", prob.value)
print("weights:", w.value)
print("solver:", prob.solver_stats.solver_name)
print("solve time:", prob.solver_stats.solve_time)
工程建议:在滚动回测中,把 mu 和 Sigma 设为 Parameter,每次更新 mu.value 和 Sigma.value 后重新调用 prob.solve(),可避免重复构建问题,显著提升效率。

六、案例复盘:一个最小可用的资金配置流程

假设你有 5 个策略,已知预期收益向量和协方差矩阵,要求权重非负、和为 1、单策略不超过 40%,并在目标中加入换手率惩罚。完整流程如下:定义变量与参数 → 构建目标与约束 → 选择求解器 → 求解并读取权重 → 检查状态与约束违反情况。若求解失败,按“先查状态、再查约束、最后查 DCP”的顺序排查。

CVXPY 建模与求解闭环 变量与参数 Variable / Parameter 目标函数 Maximize / Minimize 约束条件 等式 / 不等式 / 锥 求解器 ECOS / OSQP / SCS 结果读取 status / value / .value

复盘要点有三:第一,变量与参数要区分清楚,参数化后回测效率更高;第二,目标与约束必须满足 DCP,否则建模阶段就会失败;第三,求解器选择要与问题类型匹配,结果读取要同时关注状态、目标值和变量值。把这三点固化为检查清单,后续多策略配置的复杂模型才能稳定落地。