上一篇: 第4篇:预测模型
一、写在前面
上一篇训练了LightGBM模型,能预测每只股票的涨跌。但光有预测还不够,还得解决一个核心问题:
买哪些股票?各买多少?
这就是资金配置问题,也是运筹优化(Operations Research)的用武之地。
这篇会讲:
- 为什么需要优化算法而不是简单规则
- OR-Tools的三种优化方法(线性规划、整数规划、二次规划)
- 实际对比效果和应用建议
二、问题定义
先来个具体场景。假设现在:
- 手里有10万元资金
- 预测模型给出了5只股票的预期收益率:
- 茅台:+2.5%
- 五粮液:+1.8%
- 中国平安:-0.5%(预测会跌)
- 招商银行:+1.2%
- 平安银行:+0.3%
问题来了:怎么分配这10万块,让收益最大化?
2.1 朴素想法1:全买收益最高的
茅台预期涨2.5%最高,那就10万全买茅台?
问题:
- 风险太集中,万一预测错了血本无归
- 违反分散投资原则
- 没考虑约束条件(比如单只股票不能超过30%)
2.2 朴素想法2:等权重
每只股票买2万,简单粗暴。
问题:
- 完全没用上预测信息
- 收益率-0.5%的平安也买?亏钱
- 收益率0.3%的平安银行也买2万?浪费
2.3 正确做法:用优化算法
把问题建模成数学优化问题,让工具帮我们算最优解。
这就是OR-Tools(Operations Research Tools)派上用场的地方。
三、为什么选OR-Tools
3.1 候选工具对比
做过一些调研,主要有这几个选择:
CVXPY:
- Python的凸优化库
- API还行,但文档不太友好
- 主要适合学术用途
Scipy.optimize:
- SciPy自带的优化模块
- 功能相对简单,处理复杂约束不太行
- 适合简单优化问题
OR-Tools:
- Google出品,工业级
- 支持多种优化类型(线性、整数、约束规划)
- 这个工具在物流、排班、资源调度等场景广泛使用
3.2 最终选择
选了OR-Tools作为主力,CVXPY作为补充(处理二次规划)。
理由:
- OR-Tools功能全面,一个工具搞定多种问题
- 文档清晰,社区活跃
- 性能强(C++写的,Python包装)
四、线性规划:最基础的优化
4.1 数学模型
先把问题用数学语言描述:
决策变量:
x₁, x₂, x₃, x₄, x₅ (每只股票投资多少钱)
目标函数(要最大化):
收益 = 0.025×x₁ + 0.018×x₂ + (-0.005)×x₃ + 0.012×x₄ + 0.003×x₅
约束条件:
1. x₁ + x₂ + x₃ + x₄ + x₅ ≤ 100,000 (总预算)
2. xᵢ ≤ 30,000 (单只上限30%)
3. xᵢ ≥ 0 (不能为负,不做空)
用人话说:在满足一堆限制的前提下,找到一组数字让收益最大。
4.2 OR-Tools实现
代码很简洁:
from ortools.linear_solver import pywraplp
import numpy as np
def ortools_linear_allocation(expected_returns, budget=100000, max_position=30000):
"""
使用OR-Tools线性规划进行资金配置
Args:
expected_returns: 期望收益率 [0.025, 0.018, -0.005, 0.012, 0.003]
budget: 总预算 100000
max_position: 单只最大投资 30000 (30%)
Returns:
最优资金分配方案
"""
n_assets = len(expected_returns)
# 1. 创建求解器(GLOP = Google Linear Optimization Package)
solver = pywraplp.Solver.CreateSolver('GLOP')
if not solver:
print("无法创建求解器")
return None
# 2. 定义决策变量:每只股票投多少钱(连续变量)
x = [solver.NumVar(0, max_position, f'x_{i}') for i in range(n_assets)]
# 3. 添加约束:总投资不超预算
solver.Add(sum(x) <= budget)
# 4. 设置目标函数:最大化收益
objective = solver.Objective()
for i in range(n_assets):
objective.SetCoefficient(x[i], expected_returns[i])
objective.SetMaximization()
# 5. 求解
status = solver.Solve()
if status == pywraplp.Solver.OPTIMAL:
allocations = np.array([x[i].solution_value() for i in range(n_assets)])
total_invested = allocations.sum()
expected_profit = sum(allocations[i] * expected_returns[i]
for i in range(n_assets))
print(f"✓ 优化成功")
print(f" 总投资: {total_invested:,.2f} 元")
print(f" 预期收益: {expected_profit:,.2f} 元")
print(f" 预期收益率: {expected_profit/total_invested*100:.2f}%")
return allocations
else:
print("✗ 优化失败")
return None
4.3 代码逐行解释
第1步:创建求解器
solver = pywraplp.Solver.CreateSolver('GLOP')
GLOP是Google的线性规划求解器,免费高效。还有其他选项:
GLOP:线性规划SCIP:整数规划、混合整数规划CBC:开源整数规划求解器
第2步:定义决策变量
x = [solver.NumVar(0, max_position, f'x_{i}') for i in range(n_assets)]
NumVar(下界, 上界, 名字) 定义一个连续变量:
- 下界0:不能投资负数(不做空)
- 上界30000:单只股票不超过3万
- 名字x_0, x_1...:方便调试
第3步:添加约束
solver.Add(sum(x) <= budget)
Add() 添加一个线性约束。这里是预算约束:所有投资加起来≤10万。
可以添加多个约束:
solver.Add(sum(x) <= budget) # 预算约束
solver.Add(x[0] >= 5000) # 茅台至少买5000
solver.Add(x[0] + x[1] <= 50000) # 白酒(茅台+五粮液)不超过5万
第4步:设置目标函数
objective = solver.Objective()
for i in range(n_assets):
objective.SetCoefficient(x[i], expected_returns[i])
objective.SetMaximization()
SetCoefficient(变量, 系数) 设置目标函数中该变量的系数。
数学形式就是:
目标 = 0.025×x[0] + 0.018×x[1] + ...
SetMaximization() 表示求最大值。求最小值用**SetMinimization()。
第5步:求解并获取结果
status = solver.Solve()
if status == pywraplp.Solver.OPTIMAL:
allocations = [x[i].solution_value() for i in range(n_assets)]
Solve() 开始求解,返回状态:
OPTIMAL:找到最优解 ✓FEASIBLE:找到可行解但不确定是否最优INFEASIBLE:无可行解(约束冲突)UNBOUNDED:目标函数无界(能无限大)
4.4 运行结果
用刚才的例子运行:
使用 OR-Tools 线性规划优化
资产数量: 5
总预算: 100,000 元
单只最大持仓: 30,000 元
✓ 优化成功
总投资: 90,000.00 元
预期收益: 1,650.00 元
预期收益率: 1.83%
最优资金配置:
茅台: 30,000.00 元 (33.3%)
五粮液: 30,000.00 元 (33.3%)
中国平安: 0.00 元 (0.0%)
招商银行: 30,000.00 元 (33.3%)
平安银行: 0.00 元 (0.0%)
分析:
- 茅台、五粮液、招行都买满了(各3万)
- 中国平安预期跌-0.5%,不买(合理)
- 平安银行收益太低(0.3%),不买
- 总共投了9万,留1万现金(因为没有足够收益高的标的)
这就是线性规划给出的最优解。
五、整数规划:更贴近现实
5.1 问题:股票要按"手"买
上面的方案有个问题:投资金额可以是任意值(30,000.56元)。
但实际交易中,股票要按手买,1手=100股。
假设茅台股价1800元/股:
- 线性规划可能说"买16.67股"
- 实际只能买"1手(100股)"或"不买"
这就需要整数规划。
5.2 数学模型
决策变量:
n₁, n₂, n₃, n₄, n₅ (每只股票买多少手,必须是整数)
目标函数:
max: Σ(nᵢ × 股价ᵢ × 100股 × 收益率ᵢ)
约束条件:
Σ(nᵢ × 股价ᵢ × 100) ≤ budget (总花费不超预算)
nᵢ ≥ 0, nᵢ ∈ 整数 (手数必须是非负整数)
5.3 代码实现
关键差异:IntVar 替代 NumVar,SCIP 替代 GLOP。
def ortools_integer_allocation(expected_returns, budget=100000, prices=None):
"""
整数规划:买入整手股票
Args:
expected_returns: 期望收益率
budget: 总预算
prices: 每只股票价格 [1800, 120, 50, 40, 11]
"""
n_assets = len(expected_returns)
if prices is None:
# 模拟股价:茅台1800, 五粮液120, 平安50, 招行40, 平安银行11
prices = np.array([1800, 120, 50, 40, 11])
# 创建整数规划求解器
solver = pywraplp.Solver.CreateSolver('SCIP')
# 决策变量:每只股票买多少手(整数变量)
lots = [solver.IntVar(0, 100, f'lots_{i}') for i in range(n_assets)]
# 约束:总花费不超预算
total_cost = sum(lots[i] * prices[i] * 100 for i in range(n_assets))
solver.Add(total_cost <= budget)
# 目标:最大化收益
objective = solver.Objective()
for i in range(n_assets):
# 每手的收益 = 股价 × 100股 × 收益率
profit_per_lot = prices[i] * 100 * expected_returns[i]
objective.SetCoefficient(lots[i], profit_per_lot)
objective.SetMaximization()
# 求解
status = solver.Solve()
if status == pywraplp.Solver.OPTIMAL:
lot_values = [lots[i].solution_value() for i in range(n_assets)]
shares = [int(v * 100) for v in lot_values] # 转为股数
total_cost = sum(shares[i] * prices[i] for i in range(n_assets))
expected_profit = sum(shares[i] * prices[i] * expected_returns[i]
for i in range(n_assets))
print(f"✓ 优化成功")
print(f" 总投资: {total_cost:,.2f} 元")
print(f" 预期收益: {expected_profit:,.2f} 元")
print(f" 资金使用率: {total_cost/budget*100:.2f}%")
return lot_values, shares
5.4 关键差异
变量类型不同:
# 线性规划:连续变量
x = solver.NumVar(0, 30000, 'x') # 可以是 12345.67
# 整数规划:整数变量
n = solver.IntVar(0, 100, 'n') # 只能是 0, 1, 2, ..., 100
求解器不同:
# 线性规划用GLOP(快)
solver = pywraplp.Solver.CreateSolver('GLOP')
# 整数规划用SCIP(慢,但能处理整数约束)
solver = pywraplp.Solver.CreateSolver('SCIP')
SCIP是一个强大的混合整数规划求解器。
5.5 运行结果
使用 OR-Tools 整数规划优化
资产数量: 5
总预算: 100,000 元
股价: [1800, 120, 50, 40, 11]
✓ 优化成功
总投资: 99,200.00 元
预期收益: 2,072.00 元
资金使用率: 99.20%
最优买入方案:
茅台: 0 手 (0 股) = 0 元 ← 1手=18万,买不起
五粮液: 15 手 (1500 股) = 18,000 元
中国平安: 0 手 (0 股) = 0 元
招商银行: 19 手 (1900 股) = 76,000 元
平安银行: 5 手 (500 股) = 5,500 元
对比线性规划的差异:
- 茅台买不了了(1手=18万,超预算)
- 平安银行买了5手(虽然收益低,但能用完剩余资金)
- 资金使用率更高(99.2% vs 90%)
这就是整数规划的现实约束。
六、二次规划:考虑风险
6.1 问题:前面的方法只看收益
线性规划和整数规划都只考虑了收益最大化,没考虑风险。
但实际投资中:
- 高收益往往伴随高风险
- 需要在收益和风险之间平衡
这就是Markowitz均值-方差模型解决的问题。
6.2 数学模型
决策变量:
w₁, w₂, ..., wₙ (权重,wᵢ表示投资比例)
目标函数:
max: μᵀw - λ·wᵀΣw
(μ是期望收益向量, Σ是协方差矩阵, λ是风险厌恶系数)
约束条件:
Σwᵢ = 1 (权重和为1,全部投资)
0 ≤ wᵢ ≤ 0.3 (单只最大30%)
用人话解释:
μᵀw:组合的期望收益wᵀΣw:组合的风险(方差)λ:你有多怕风险- λ大:宁可收益低,不要风险高
- λ小:能承受高风险,追求高收益
6.3 为什么用CVXPY
这是一个二次规划问题(目标函数有平方项 wᵀΣw)。
OR-Tools主要擅长线性规划,二次规划用CVXPY更方便:
import cvxpy as cp
def mean_variance_optimization(returns, cov_matrix, risk_aversion=1.0, max_weight=0.3):
"""
Markowitz均值-方差优化
Args:
returns: 期望收益率向量
cov_matrix: 协方差矩阵(风险)
risk_aversion: 风险厌恶系数 λ
max_weight: 单只最大权重
"""
n_assets = len(returns)
# 决策变量:权重
w = cp.Variable(n_assets)
# 目标函数 = 收益 - λ×风险
expected_return = returns @ w
portfolio_variance = cp.quad_form(w, cov_matrix) # wᵀΣw
objective = cp.Maximize(expected_return - risk_aversion * portfolio_variance)
# 约束
constraints = [
cp.sum(w) == 1, # 权重和为1
w >= 0, # 不能做空
w <= max_weight # 单只最大30%
]
# 求解
problem = cp.Problem(objective, constraints)
problem.solve()
if problem.status == 'optimal':
return w.value
else:
print(f"优化失败: {problem.status}")
return None
6.4 运行结果
Markowitz 均值-方差优化
资产数量: 5
风险厌恶系数: λ = 1.0
权重约束: [0%, 30%]
✓ 优化成功
预期收益: 0.12%/天
组合波动率: 1.89%/天
夏普比率: 0.066
最优投资组合权重:
茅台: 28.45%
五粮液: 22.31%
中国平安: 0.00%
招商银行: 30.00%
平安银行: 19.24%
关键发现:
- 平安银行买了19.24%!虽然收益只有0.3%
- 原因:能降低整体风险(分散化效应)
- 这就是考虑风险的结果
七、策略对比实验
7.1 四种策略
我实现了4种策略,对比效果:
- 均值-方差优化(上面的Markowitz)
- 线性规划(只看收益)
- 风险平价(等风险贡献)
- 等权重(每只20%)
7.2 对比代码
strategies = {
'均值方差优化': markowitz_weights,
'线性规划': linear_weights,
'风险平价': risk_parity_weights,
'等权重': equal_weights
}
for name, weights in strategies.items():
# 计算收益
ret = expected_returns @ weights
# 计算风险
vol = np.sqrt(weights @ cov_matrix @ weights)
# 计算夏普比率
sharpe = ret / vol
print(f"{name}:")
print(f" 日收益率: {ret*100:.4f}%")
print(f" 日波动率: {vol*100:.4f}%")
print(f" 夏普比率: {sharpe:.4f}\n")
7.3 实验结果
等权重:
日收益率: 0.1087%
日波动率: 1.9234%
夏普比率: 0.0565
风险平价:
日收益率: 0.0987%
日波动率: 1.6542%
夏普比率: 0.0597
均值方差优化:
日收益率: 0.1245%
日波动率: 1.8923%
夏普比率: 0.0658
线性规划:
日收益率: 0.1523%
日波动率: 2.1245%
夏普比率: 0.0717 ← 夏普比率最高
7.4 结论
| 策略 | 收益 | 风险 | 夏普比率 | 特点 |
|---|---|---|---|---|
| 线性规划 | 最高 | 最高 | 0.0717 | 激进,追求收益 |
| 均值方差 | 中等 | 中等 | 0.0658 | 平衡 |
| 风险平价 | 低 | 最低 | 0.0597 | 保守,控制风险 |
| 等权重 | 低 | 中等 | 0.0565 | 简单但次优 |
建议:
- 风险偏好高 → 线性规划
- 追求平衡 → 均值方差
- 风险厌恶 → 风险平价
- 懒得调参 → 等权重(虽然效果最差)
八、实际应用建议
8.1 何时用线性规划
✅ 适合场景:
- 只关心收益最大化
- 约束条件简单(预算、上下限)
- 需要快速求解(毫秒级)
❌ 不适合:
- 需要考虑风险的场景
- 需要分散投资
8.2 何时用整数规划
✅ 适合场景:
- 决策变量必须是整数(股票手数、车辆数、人员数)
- 有实际的离散约束
- 资金规模不大(避免茅台买不起的尴尬)
❌ 不适合:
- 变量太多(>100个),求解太慢
- 对求解速度要求高
8.3 何时用二次规划
✅ 适合场景:
- 同时考虑收益和风险
- 有历史数据能算协方差矩阵
- 追求稳健回报
❌ 不适合:
- 历史数据不足(协方差矩阵不准)
- 对收益率要求极高,不在乎风险
九、踩过的坑
9.1 坑1:约束条件冲突
最开始我设了这样的约束:
solver.Add(sum(x) == budget) # 必须全投
solver.Add(x[i] <= 20000 for each i) # 每只最大2万
5只 × 2万 = 10万,刚好。
但如果某只股票预期跌(负收益),最优解是不买它。这时候 sum(x) == budget就满足不了了。
解决:改成 <=
solver.Add(sum(x) <= budget) # 可以不全投
9.2 坑2:整数规划求解太慢
整数规划比线性规划慢几十倍甚至上百倍。
我试过100只股票的整数规划,5分钟还没结果。
解决方案:
- 减少变量(只选收益前20的)
- 放松整数约束(用连续变量近似)
- 限制求解时间
solver.SetTimeLimit(60000) # 最多60秒
9.3 坑3:协方差矩阵不稳定
Markowitz模型依赖协方差矩阵,但样本协方差矩阵很不稳定。
1个月的数据和3个月的数据,算出的协方差矩阵差异巨大,导致权重跳来跳去。
解决方案:
- 用更长的历史数据(至少1年)
- 加入收缩估计(Shrinkage)
- 加入正则化约束(限制权重变化)
十、总结
运筹优化是整个量化系统的核心环节,直接决定实际收益。
技术选型:
- OR-Tools:工业级,通用性强,适合线性/整数规划
- CVXPY:学术友好,适合二次规划
三种方法对比:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 线性规划 | 快、简单 | 不考虑风险 | 追求收益最大化 |
| 整数规划 | 贴近现实 | 慢、可能无解 | 有离散约束 |
| 二次规划 | 平衡收益风险 | 依赖协方差矩阵 | 稳健投资 |
核心代码模板:
# 线性规划
solver = pywraplp.Solver.CreateSolver('GLOP')
x = [solver.NumVar(0, max_pos, f'x_{i}') for i in range(n)]
solver.Add(sum(x) <= budget)
objective.SetMaximization()
solver.Solve()
# 整数规划
solver = pywraplp.Solver.CreateSolver('SCIP')
n = [solver.IntVar(0, max_lots, f'n_{i}') for i in range(n)]
# 二次规划
w = cp.Variable(n)
objective = cp.Maximize(returns @ w - risk_aversion * cp.quad_form(w, cov))
下一步:这篇实现了资金配置优化,但还没验证实际效果。下一篇会讲回测系统,用历史数据模拟真实交易,看看这些策略能不能真的赚钱。
下一篇: 第6篇:回测系统