1. 这节究竟解决什么实际问题?¶
10.2 线性规划与投资组合优化 的具体任务是:把投资限制翻译为线性目标和约束,并检查可行性。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
掌握基础代数、概率和 Python 数组操作。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
使用模拟或固定教学数据;重点是说明模型假设、估计误差和数值误差。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 10.2 线性规划与投资组合优化 的输入、计算步骤、输出和局限;尤其能说明:把投资限制翻译为线性目标和约束,并检查可行性。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
数学方法给出明确的计算规则;结论是否有效仍取决于研究对象、输入数据和模型假设。
先给出符号、单位、假设和可识别条件,再把公式翻译为逐步代码。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
数值库帮你稳定求解;理解目标函数、约束和诊断仍不可省略。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“把投资限制翻译为线性目标和约束,并检查可行性。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
随机种子未固定、矩阵维度错配、优化约束遗漏、把数值收敛当成模型正确。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
数学模型的简化假设也会引入偏差;需要做参数敏感性与极端情景检查。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/10_math_methods/02_linear_programming_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/10_math_methods/02_linear_programming_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:QuantEcon Python Lectures。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 03_markov_chains。本节的概念会成为理解它的输入,而不是孤立的名词。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import linprog, minimize
np.random.seed(42)
print('Libraries loaded')
Libraries loaded
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
1. 线性规划(LP)的基本形式¶
$$\min_{x} \quad c^\top x$$ $$\text{s.t.} \quad A_{ub}x \leq b_{ub}, \quad A_{eq}x = b_{eq}, \quad l \leq x \leq u$$
在量化组合优化中,LP 比 QP(二次规划)更适合以下场景:
- 最大化预期超额收益(线性目标),约束换手率、行业中性、因子中性
- 在交易成本为线性假设下的最优执行
- 多空策略的最优杠杆分配
# 简单示例:在预算约束下最大化预期收益
np.random.seed(42)
n = 10 # 10 只股票
expected_returns = np.random.normal(0.10, 0.05, n) # 预期年化收益率
# LP:最大化 μ^T x 即最小化 -μ^T x
# 约束: sum(x) = 1 (满仓)
# 0 <= x_i <= 0.15 (最大单股 15%)
c_lp = -expected_returns # 最小化负收益 = 最大化收益
A_eq = np.ones((1, n)) # sum(x) = 1
b_eq = [1.0]
bounds = [(0, 0.15) for _ in range(n)] # 0 ≤ x_i ≤ 15%
result = linprog(c_lp, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method='highs')
x_lp = result.x
print('LP 最优组合权重:')
for i, (ret, w) in enumerate(zip(expected_returns, x_lp)):
if w > 0.001:
print(f' 股票{i+1}: 预期收益={ret:.3f}, 权重={w:.4f}')
print(f'\n预期组合收益: {np.dot(x_lp, expected_returns):.4f}')
print(f'注意: LP 通常产生集中投资(只持有预期收益最高的几只股票)')
LP 最优组合权重: 股票1: 预期收益=0.125, 权重=0.1500 股票2: 预期收益=0.093, 权重=0.1000 股票3: 预期收益=0.132, 权重=0.1500 股票4: 预期收益=0.176, 权重=0.1500 股票7: 预期收益=0.179, 权重=0.1500 股票8: 预期收益=0.138, 权重=0.1500 股票10: 预期收益=0.127, 权重=0.1500 预期组合收益: 0.1410 注意: LP 通常产生集中投资(只持有预期收益最高的几只股票)
# 模拟因子暴露和 Alpha
np.random.seed(42)
n_stocks = 20
n_factors = 3 # 市场、规模、价值
alpha = np.random.normal(0.02, 0.01, n_stocks) # 各股 Alpha
factor_loadings = np.random.normal(0, 1, (n_stocks, n_factors)) # 因子载荷
benchmark_weights = np.ones(n_stocks) / n_stocks # 等权基准
benchmark_factor_exp = factor_loadings.T @ benchmark_weights
# LP 目标:最大化 Alpha = 最小化 -Alpha
c = -alpha
# 等式约束:
# 1. 权重之和 = 1
# 2. 因子暴露 = 基准因子暴露(因子中性)
A_eq_parts = [np.ones((1, n_stocks))] # sum(w) = 1
for j in range(n_factors):
A_eq_parts.append(factor_loadings[:, j].reshape(1, -1))
A_eq = np.vstack(A_eq_parts)
b_eq = np.concatenate([[1.0], benchmark_factor_exp])
bounds_lp = [(-0.05, 0.10) for _ in range(n_stocks)] # 允许小空头
try:
result = linprog(c, A_eq=A_eq, b_eq=b_eq, bounds=bounds_lp, method='highs')
if result.success:
w_optimal = result.x
portfolio_alpha = np.dot(w_optimal, alpha)
print('优化求解成功。')
print(f'组合 Alpha: {portfolio_alpha:.4f} ({portfolio_alpha:.2%})')
print(f'等权基准 Alpha: {np.dot(benchmark_weights, alpha):.4f} ({np.dot(benchmark_weights, alpha):.2%})')
print(f'超额 Alpha: {portfolio_alpha - np.dot(benchmark_weights, alpha):.4f}')
print(f'\n持仓分布: 正多头={sum(w_optimal>0.01)} 只, 轻空头={sum(w_optimal<-0.001)} 只')
else:
print('优化未收敛:', result.message)
except Exception as e:
print(f'Error: {e}')
优化求解成功。 组合 Alpha: 0.0283 (2.83%) 等权基准 Alpha: 0.0183 (1.83%) 超额 Alpha: 0.0100 持仓分布: 正多头=14 只, 轻空头=6 只
3. LP vs QP(Markowitz)对比¶
# 对比 LP 和 Markowitz QP 在相同约束下的解
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
expected_ret = np.random.normal(0.08, 0.04, n_stocks)
# LP 解(纯最大化 Alpha,忽略风险)
c_lp = -expected_ret
bounds_simple = [(0, 0.15)] * n_stocks
A_eq_simple = np.ones((1, n_stocks))
b_eq_simple = [1.0]
res_lp = linprog(c_lp, A_eq=A_eq_simple, b_eq=b_eq_simple, bounds=bounds_simple, method='highs')
# QP:minimize -μ^T w + λ w^T Σ w(Markowitz,简化用对角协方差)
vols = np.random.uniform(0.1, 0.3, n_stocks)
Cov = np.diag(vols**2) # 简化:无相关性
lambda_risk = 5 # 风险厌恶系数
def neg_utility(w):
return -np.dot(w, expected_ret) + lambda_risk * w @ Cov @ w
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds_qp = [(0, 0.15)] * n_stocks
w0 = np.ones(n_stocks) / n_stocks
res_qp = minimize(neg_utility, w0, method='SLSQP',
constraints=constraints, bounds=bounds_qp)
axes[0].bar(range(n_stocks), res_lp.x, color='steelblue', alpha=0.7, label='LP(纯收益最大)')
axes[0].bar(range(n_stocks), res_qp.x, color='darkorange', alpha=0.7, label='QP Markowitz')
axes[0].set_title('LP vs QP 持仓权重对比')
axes[0].set_xlabel('股票'); axes[0].set_ylabel('权重'); axes[0].legend()
concentration = pd.Series([res_lp.x, res_qp.x],
index=['LP(最大收益)', 'QP Markowitz'])
concentration.apply(lambda w: sum(np.array(w) > 0.01)).plot(kind='bar', ax=axes[1], color=['steelblue', 'darkorange'])
axes[1].set_title('持仓只数对比(权重>1%)'); axes[1].set_ylabel('持仓数')
plt.tight_layout(); plt.show()
print('结论:LP 给出极端集中组合,QP 考虑风险后给出更分散的组合')
结论:LP 给出极端集中组合,QP 考虑风险后给出更分散的组合
练习¶
- 在 LP 优化中增加「最大换手率」约束(与等权基准相比),观察最优 Alpha 如何随换手率上限变化。
- 实现一个「最小化组合跟踪误差」的 LP(允许小幅超配/低配),目标是最接近基准同时最大化 Alpha。
- 研究 scipy 的
milp(混合整数线性规划),当允许持仓数量 ≤ 10 时重新求解上述问题。
下一节 → 03_markov_chains.ipynb