In [1]:
Copied!
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from scipy.optimize import minimize
from sklearn.covariance import LedoitWolf
plt.rcParams['figure.figsize'] = (10, 7)
plt.style.use('seaborn-v0_8-muted')
# 下载多资产数据以构建组合
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'JPM', 'GLD', 'TLT']
prices = yf.download(tickers, start='2020-01-01', end='2024-01-01',
progress=False)['Close'].dropna()
returns = prices.pct_change().dropna()
n_assets = len(tickers)
mu = returns.mean() * 252 # 年化期望收益率
print(f'已获取 {n_assets} 只涵盖科技、银行、黄金、债市的资产,准备构建跨市场组合。')
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from scipy.optimize import minimize
from sklearn.covariance import LedoitWolf
plt.rcParams['figure.figsize'] = (10, 7)
plt.style.use('seaborn-v0_8-muted')
# 下载多资产数据以构建组合
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'JPM', 'GLD', 'TLT']
prices = yf.download(tickers, start='2020-01-01', end='2024-01-01',
progress=False)['Close'].dropna()
returns = prices.pct_change().dropna()
n_assets = len(tickers)
mu = returns.mean() * 252 # 年化期望收益率
print(f'已获取 {n_assets} 只涵盖科技、银行、黄金、债市的资产,准备构建跨市场组合。')
已获取 7 只涵盖科技、银行、黄金、债市的资产,准备构建跨市场组合。
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. 估值陷阱:为什么要用“收缩估计量”?¶
QuantEcon 深度提示: MPT 最大的痛点是“垃圾进,垃圾出”。普通的样本协方差矩阵(Sample Covariance)在处理多资产时非常不稳定,容易放大噪声。
Ledoit-Wolf 收缩:这是一种更稳健的方法。它将样本协方差向一个结构化的目标(如等相关矩阵)进行“拉伸”,从而降低极端错误。这在资产数量较多时至关重要。
In [3]:
Copied!
# 传统方法 vs 稳健方法
cov_sample = returns.cov() * 252
lw = LedoitWolf()
cov_lw = lw.fit(returns).covariance_ * 252
print("稳健协方差矩阵计算完成。这能有效降低模型对历史噪声的过度敏感。")
# 传统方法 vs 稳健方法
cov_sample = returns.cov() * 252
lw = LedoitWolf()
cov_lw = lw.fit(returns).covariance_ * 252
print("稳健协方差矩阵计算完成。这能有效降低模型对历史噪声的过度敏感。")
稳健协方差矩阵计算完成。这能有效降低模型对历史噪声的过度敏感。
2. 组合绩效函数¶
$$\mu_p = \mathbf{w}^T \boldsymbol{\mu}, \quad \sigma_p = \sqrt{\mathbf{w}^T \boldsymbol{\Sigma} \mathbf{w}}$$
In [4]:
Copied!
def portfolio_stats(weights, mu, cov):
w = np.array(weights)
ret = w @ mu
vol = np.sqrt(w @ cov @ w)
sharpe = (ret - 0.04) / vol # 假设无风险利率为 4%
return ret, vol, sharpe
def portfolio_stats(weights, mu, cov):
w = np.array(weights)
ret = w @ mu
vol = np.sqrt(w @ cov @ w)
sharpe = (ret - 0.04) / vol # 假设无风险利率为 4%
return ret, vol, sharpe
In [5]:
Copied!
constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
bounds = [(0, 1)] * n_assets
w0 = np.ones(n_assets) / n_assets
# 优化最大夏普 (使用稳健协方差 cov_lw)
res_sr = minimize(lambda w: -portfolio_stats(w, mu, cov_lw)[2], w0,
bounds=bounds, constraints=constraints)
w_sr = res_sr.x
sr_ret, sr_vol, _ = portfolio_stats(w_sr, mu, cov_lw)
# 优化最小方差
res_mv = minimize(lambda w: portfolio_stats(w, mu, cov_lw)[1], w0,
bounds=bounds, constraints=constraints)
w_mv = res_mv.x
mv_ret, mv_vol, _ = portfolio_stats(w_mv, mu, cov_lw)
print("优化计算完成。")
constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
bounds = [(0, 1)] * n_assets
w0 = np.ones(n_assets) / n_assets
# 优化最大夏普 (使用稳健协方差 cov_lw)
res_sr = minimize(lambda w: -portfolio_stats(w, mu, cov_lw)[2], w0,
bounds=bounds, constraints=constraints)
w_sr = res_sr.x
sr_ret, sr_vol, _ = portfolio_stats(w_sr, mu, cov_lw)
# 优化最小方差
res_mv = minimize(lambda w: portfolio_stats(w, mu, cov_lw)[1], w0,
bounds=bounds, constraints=constraints)
w_mv = res_mv.x
mv_ret, mv_vol, _ = portfolio_stats(w_mv, mu, cov_lw)
print("优化计算完成。")
优化计算完成。
4. 可视化:有效前沿与资产配置¶
In [6]:
Copied!
# 模拟 2000 个随机组合进行对比
n_sims = 2000
sim_results = np.zeros((n_sims, 2))
for i in range(n_sims):
w = np.random.random(n_assets)
w /= w.sum()
r, v, _ = portfolio_stats(w, mu, cov_lw)
sim_results[i] = [r, v]
plt.scatter(sim_results[:, 1], sim_results[:, 0], c=sim_results[:, 0]/sim_results[:, 1],
cmap='viridis', alpha=0.3, s=10)
plt.scatter(sr_vol, sr_ret, marker='*', s=300, color='gold', label='最大夏普组合')
plt.scatter(mv_vol, mv_ret, marker='D', s=150, color='cyan', label='最小方差组合')
plt.xlabel('预期波动率 (Risk)')
plt.ylabel('预期收益率 (Return)')
plt.title("现代投资组合理论:有效前沿")
plt.legend()
plt.grid(alpha=0.2)
plt.show()
print('\n--- 最大夏普组合权重分布 ---')
for t, w in sorted(zip(tickers, w_sr), key=lambda x: -x[1]):
if w > 0.01: print(f"{t:<8}: {w:.2%}")
# 模拟 2000 个随机组合进行对比
n_sims = 2000
sim_results = np.zeros((n_sims, 2))
for i in range(n_sims):
w = np.random.random(n_assets)
w /= w.sum()
r, v, _ = portfolio_stats(w, mu, cov_lw)
sim_results[i] = [r, v]
plt.scatter(sim_results[:, 1], sim_results[:, 0], c=sim_results[:, 0]/sim_results[:, 1],
cmap='viridis', alpha=0.3, s=10)
plt.scatter(sr_vol, sr_ret, marker='*', s=300, color='gold', label='最大夏普组合')
plt.scatter(mv_vol, mv_ret, marker='D', s=150, color='cyan', label='最小方差组合')
plt.xlabel('预期波动率 (Risk)')
plt.ylabel('预期收益率 (Return)')
plt.title("现代投资组合理论:有效前沿")
plt.legend()
plt.grid(alpha=0.2)
plt.show()
print('\n--- 最大夏普组合权重分布 ---')
for t, w in sorted(zip(tickers, w_sr), key=lambda x: -x[1]):
if w > 0.01: print(f"{t:<8}: {w:.2%}")
--- 最大夏普组合权重分布 --- AAPL : 43.13% GOOGL : 29.87% GLD : 27.00%
🎯 练习¶
- 对比实验:将
cov_lw换回cov_sample,观察最优权重是否发生了剧烈漂移? - 分散化度量:计算组合的 Herfindahl 指数 ($H = \sum w_i^2$)。H 越小,说明权重分配越分散。计算最大夏普组合的 H 值是多少?
- 现实惩罚:在优化中加入单只股票不超过 20% 的限制,观察有效前沿如何向内收缩。
下一节 → 02_risk_parity.ipynb(我们将学习不依赖期望收益,仅根据风险贡献来平衡组合的方法)
In [ ]:
Copied!