第四章:Monte Carlo模拟投资组合——随机权重生成、组合收益率与风险计算、模拟次数选择
好,咱们进入正题。Monte Carlo模拟在投资组合优化里,说白了就是“暴力枚举”。你想想看,市场有几千只股票,权重组合几乎是无限的。传统优化方法(比如均值-方差)需要解方程,但Monte Carlo的思路很简单——随机生成一大堆权重,算算每个组合的表现,然后挑好的。
我个人习惯把这一步叫做“用计算量换洞察”。别小看这个笨办法,我在项目中遇到过好几次,当协方差矩阵接近奇异时,传统优化直接崩了,反而是Monte Carlo稳稳地给出了可行解。
4.1 随机权重生成——别让代码坑了你
生成随机权重,第一反应可能是:
import numpy as np
weights = np.random.random(n_assets)
weights /= weights.sum() # 归一化到1
嗯,这里要注意。这段代码确实能生成一组和为1的权重,但它的分布并不均匀。为什么?因为Dirichlet分布才是正确的打开方式。
核心要点:用 np.random.dirichlet(alpha, size) 生成权重。alpha取全1向量时,每个权重向量在单纯形上均匀分布。
def generate_random_weights(n_assets, n_portfolios):
"""
生成n_portfolios组随机权重
每组权重和为1,且非负
"""
alpha = np.ones(n_assets)
weights = np.random.dirichlet(alpha, size=n_portfolios)
return weights
# 示例:5只股票,生成10000组权重
weights = generate_random_weights(5, 10000)
print(weights.shape) # (10000, 5)
print(weights[0].sum()) # 1.0
我曾经犯过一个低级错误——直接用均匀分布生成权重然后归一化。结果发现,当资产数量超过10个时,大部分权重都集中在0附近,极端值很少。这会导致模拟出的有效前沿“缺角”。后来改用Dirichlet,问题就解决了。
避坑指南:我曾经在生成权重时忘了检查负值。虽然Dirichlet默认非负,但如果你自己写生成逻辑,一定要加 np.clip 或断言。负权重意味着做空,这在某些场景下是不允许的。
4.2 组合收益率与风险计算——公式很简单,细节要命
有了权重,下一步就是算每个组合的收益率和风险。公式其实就两行:
- 组合收益率: \( R_p = w^T \cdot \mu \)
- 组合方差: \( \sigma_p^2 = w^T \cdot \Sigma \cdot w \)
其中 \(\mu\) 是预期收益率向量,\(\Sigma\) 是协方差矩阵。代码实现:
def portfolio_stats(weights, mean_returns, cov_matrix):
"""
计算组合的收益率和标准差
weights: (n_assets,) 或 (n_portfolios, n_assets)
"""
if weights.ndim == 1:
weights = weights.reshape(1, -1)
returns = np.dot(weights, mean_returns)
# 向量化计算方差:对每个权重向量计算 w^T @ cov @ w
variances = np.einsum('ij,jk,ik->i', weights, cov_matrix, weights)
stds = np.sqrt(variances)
return returns, stds
这里我用了 np.einsum,而不是循环。为什么?因为10000组权重,用for循环要跑几秒,而einsum是毫秒级。我在项目中遇到过性能瓶颈,当时模拟10万次,循环跑了半分钟,改成向量化后瞬间出结果。
小技巧:如果你不熟悉einsum,也可以用 np.diag(weights @ cov_matrix @ weights.T)。但注意,这会把整个矩阵乘出来,内存消耗大。einsum更高效。
算完所有组合的收益率和风险后,你会得到一个散点图。每个点代表一个随机组合。有效前沿就是这些点的上包络线。
4.3 模拟次数选择——不是越多越好
这是新手最容易纠结的问题。模拟1000次够吗?10000次?还是100万次?
我的经验是:看你的资产数量。
| 资产数量 | 推荐模拟次数 | 说明 |
|---|---|---|
| 2-5只 | 5000 - 10000 | 维度低,收敛快 |
| 6-15只 | 20000 - 50000 | 中等维度,需要更多采样 |
| 16-50只 | 100000 - 500000 | 高维空间,采样稀疏 |
| 50只以上 | 100万+ | 建议改用优化算法 |
为什么会有这个规律?你想想看,在N维空间中,随机采样要覆盖整个单纯形,需要的点数随维度指数增长。这就是所谓的“维度灾难”。
我曾经帮一个客户做50只股票的模拟,他坚持要100万次。结果跑了10分钟,有效前沿还是坑坑洼洼的。后来我改用遗传算法,只算了2万次就找到了不错的解。
判断收敛的方法:跑两次模拟,看有效前沿是否稳定。如果两次结果差异很大,说明次数不够。我个人习惯用 np.percentile 检查最大夏普比率的波动范围。
def check_convergence(n_assets, n_trials_list):
"""
检查不同模拟次数下的稳定性
"""
for n in n_trials_list:
weights = generate_random_weights(n_assets, n)
returns, stds = portfolio_stats(weights, mean_returns, cov_matrix)
sharpe = returns / stds
max_sharpe = np.max(sharpe)
print(f"模拟{n}次,最大夏普比率: {max_sharpe:.4f}")
# 实际运行会发现,从5万次到10万次,结果变化很小
check_convergence(10, [1000, 5000, 10000, 50000, 100000])
嗯,这里要注意。模拟次数不是唯一因素。如果你的协方差矩阵估计不准,模拟1亿次也没用。数据质量永远比计算量重要。
4.4 完整流程与可视化
把上面所有步骤串起来,就是一个完整的Monte Carlo模拟流程:
# 完整模拟流程
n_assets = 10
n_portfolios = 50000
# 1. 生成随机权重
weights = generate_random_weights(n_assets, n_portfolios)
# 2. 计算收益率和风险
returns, risks = portfolio_stats(weights, mean_returns, cov_matrix)
# 3. 计算夏普比率(假设无风险利率为0)
sharpe_ratios = returns / risks
# 4. 找到最优组合
best_idx = np.argmax(sharpe_ratios)
best_weights = weights[best_idx]
best_return = returns[best_idx]
best_risk = risks[best_idx]
print(f"最优组合: 收益率={best_return:.4f}, 风险={best_risk:.4f}, 夏普={sharpe_ratios[best_idx]:.4f}")
可视化部分,我习惯用散点图展示所有组合,然后用红色标记最优组合。这样一眼就能看出有效前沿的位置。
个人习惯:我会把模拟结果保存下来,方便后续分析。比如用 np.savez 存成压缩文件,包含权重、收益率、风险三组数据。下次想调整参数时,不用重新跑模拟。
4.5 本章知识体系
下面这张图概括了Monte Carlo模拟投资组合的核心逻辑:
这张图把整个流程串起来了。从输入数据开始,经过随机权重生成、指标计算,再到结果分析和收敛检查,最后输出最优权重。注意那个循环箭头——模拟次数决定了你要跑多少轮。
好了,Monte Carlo模拟的核心部分就这些。下一章我们会讨论如何从模拟结果中提取有效前沿,以及如何用这些结果做投资决策。记住,模拟只是工具,真正的价值在于你对结果的理解和判断。
公众号:蓝海资料掘金营,微信deep3321