In [1]:
Copied!
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')
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
In [2]:
Copied!
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
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(二次规划)更适合以下场景:
- 最大化预期超额收益(线性目标),约束换手率、行业中性、因子中性
- 在交易成本为线性假设下的最优执行
- 多空策略的最优杠杆分配
In [3]:
Copied!
# 简单示例:在预算约束下最大化预期收益
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 通常产生集中投资(只持有预期收益最高的几只股票)')
# 简单示例:在预算约束下最大化预期收益
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 通常产生集中投资(只持有预期收益最高的几只股票)
In [4]:
Copied!
# 模拟因子暴露和 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(f'优化成功!')
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
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(f'优化成功!')
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)对比¶
In [5]:
Copied!
# 对比 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 和 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
In [ ]:
Copied!