Open Source, Open Future!
  menu
118 文章
ღゝ◡╹)ノ❤️

第5篇:运筹优化 - 用OR-Tools解决资金配置问题

上一篇: 第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作为补充(处理二次规划)。

理由:

  1. OR-Tools功能全面,一个工具搞定多种问题
  2. 文档清晰,社区活跃
  3. 性能强(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 替代 NumVarSCIP 替代 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种策略,对比效果:

  1. 均值-方差优化(上面的Markowitz)
  2. 线性规划(只看收益)
  3. 风险平价(等风险贡献)
  4. 等权重(每只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分钟还没结果。

解决方案:

  1. 减少变量(只选收益前20的)
  2. 放松整数约束(用连续变量近似)
  3. 限制求解时间
solver.SetTimeLimit(60000)  # 最多60秒

9.3 坑3:协方差矩阵不稳定

Markowitz模型依赖协方差矩阵,但样本协方差矩阵很不稳定。

1个月的数据和3个月的数据,算出的协方差矩阵差异巨大,导致权重跳来跳去。

解决方案:

  1. 用更长的历史数据(至少1年)
  2. 加入收缩估计(Shrinkage)
  3. 加入正则化约束(限制权重变化)

十、总结

运筹优化是整个量化系统的核心环节,直接决定实际收益。

技术选型:

  • 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篇:回测系统