
简介围绕均值方差资产配置模型的可执行资料包面向金融投资、量化分析与资产组合管理方向的学习者与从业者帮助理解如何在风险与收益之间权衡并用定量方法确定最优资产权重。压缩包内共2个文件分别为MATLAB优化脚本和Excel数据结果文件脚本用于求解预期收益与方差最小化问题数据结果则存放历史收益率、风险参数及优化后的配置权重便于对照验证。整包仅36KB轻量紧凑。已有325人学习下载可作为入门现代投资组合理论的实操样例。通过运行脚本并查看结果读者能够复现马科维茨模型的求解流程掌握预期收益估算、协方差矩阵处理、权重优化与结果解读等关键环节同时能直观理解不同资产相关性对组合风险的影响为后续构建更复杂的多资产配置策略打下基础。1. 直接拿历史数据跑出一个资产配置组合别急着上机器学习做投资研究和量化交易的同学大概率都遇到过这样的场景手里有几十个资产的收益率序列老板让你给出一组权重说“要风险低一点收益也别太难看”。如果直接用等权或者拍脑袋配领导大概率会追问一句“依据是什么”。靠 Excel 里的规划求解硬算协方差矩阵一大就卡死边界还不一定对。这就是资产配置模型要解决的核心问题给定一堆资产的预期收益和风险特征怎么算出每一类资产该配多少钱。本文不扯机器学习那套高维预测直接用最经典、落地最广的马科维茨均值-方差框架作为主线从数学形式、Python 代码实现、参数估计改进到滚动回测和约束优化把一条能在本地完整跑通的最小链路给你。适合已经有 Python 和 pandas 基础、想自己动手复现资产配置模型的读者也适合那些已经会调 sklearn 但搞不清楚协方差矩阵在配置里到底扮演什么角色的工程师。2. 先搞清楚均值-方差模型在算什么以及为什么它没有过时均值-方差模型之所以几十年后还是机构做战略资产配置的默认起点不是因为它的假设真实而是因为它把“配置”这个模糊问题变成了一个边界清晰的数学问题。你只需要回答两个输入每个资产的预期收益率是多少资产之间的协方差结构长什么样。输出就是一条有效前沿前沿上的每一个点都是在给定收益水平下方差最小的组合。2.1 有效前沿的数学形式与参数边界把问题写成标准形式。假设有 n 个资产权重向量为 w预期收益向量为 μ协方差矩阵为 Σ。最小化组合方差的目标函数是min w^T Σ w约束条件是 w^T μ μ_p目标收益以及 w^T 1 1权重和为 1。这是一个典型的二次规划问题目标函数是二次的约束是线性的因此一定有全局最优解不用担心中途陷入局部极小。这里需要强调的是协方差矩阵 Σ 是整个模型的灵魂。对角线上的元素是单一资产的方差决定的是单个资产自身的波动非对角线上的元素是资产之间的协方差决定了资产之间的对冲效果。把两个相关性为负的资产放进同一个组合组合方差可以小于其中任何一个资产单独的方差这是分散化唯一的数学来源。这也是为什么在实现层面直接用历史收益率算样本协方差矩阵虽然代码只有一行但结果往往不靠谱的原因后面章节会专门展开。注意模型里的 μ 和 Σ 都是前置输入不是模型本身推导出来的。模型只负责在给定输入下寻找最优权重不负责预测未来。这一点是很多初学者误解最深的地方。2.2 为什么说全局最小方差组合是底线在有效前沿上最左端的那个点叫作全局最小方差组合Global Minimum Variance PortfolioGMV。它不依赖任何收益预测只需要协方差矩阵就能算出来因此它的估计误差来源只有一个Σ 本身。而前沿上其他所有点都需要 μ 的估计而 μ 的估计误差远比 Σ 的估计误差大。从实践角度看绝大多数机构做再平衡的时候权重实际上落在 GMV 附近的某个位置。原因很简单历史平均收益率对未来的预测能力极弱但协方差结构相对稳定。你在回测里调出来的那个夏普比率最高的组合换一段样本区间可能就不是它了而 GMV 的权重偏离幅度要小得多。这一点在后面参数估计章节里还会以 Ledoit-Wolf 收缩为例进一步验证。3. 用 scipy.optimize 从零构建一个资产配置优化器这一段直接进入代码层面。这里不引入额外的金融库只依赖 numpy、pandas 和 scipy目的是把求解逻辑彻底摊开让你清楚每一步到底在算什么。3.1 准备数据收益率矩阵和协方差矩阵首先构造一个模拟数据方便你看清数据格式要求。实际使用时把 df 换成你自己读取的行情数据就行但收益率矩阵的格式必须保证每一列是一个资产每一行是一个观察周期日、周或月。import numpy as np import pandas as pd # 模拟 5 个资产、1000 个交易日的收益率数据 np.random.seed(42) n_assets 5 n_days 1000 mean_returns np.array([0.0006, 0.0004, 0.0003, 0.0005, 0.0002]) cov_matrix np.random.randn(n_assets, n_assets) cov_matrix cov_matrix cov_matrix.T # 保证半正定 cov_matrix cov_matrix / cov_matrix.max() * 0.001 # 缩放到合理量级 returns_data np.random.multivariate_normal(mean_returns, cov_matrix, n_days) returns_df pd.DataFrame(returns_data, columns[fasset_{i} for i in range(n_assets)]) # 核心输入年化收益率和年化协方差矩阵 mu returns_df.mean() * 252 sigma returns_df.cov() * 252 print(年化收益率序列) print(mu.round(6)) print(\n年化协方差矩阵前两行) print(sigma.iloc[:2, :].round(6))这段代码里有两个关键的参数化动作* 252是日频数据的年化处理252 表示一年大约的交易天数。年化收益率直接用均值乘以交易日数年化协方差矩阵是日协方差乘以交易日数。如果是周频数据这里就换成 52月频换成 12不要照抄参数。协方差矩阵必须保证半正定模拟数据里用cov_matrix cov_matrix.T来构造目的就是防止后面求解时报“矩阵不是正定矩阵”的错误。3.2 求解最小方差组合与最大夏普组合这里使用 scipy.optimize.minimize 函数做数值优化而不是解析求解原因在于后面加约束条件比如单个资产权重上限时解析解不存在而数值方法可以平稳过渡。from scipy.optimize import minimize # 目标函数给定权重 w计算组合方差 def portfolio_variance(w, sigma): return w sigma w # 约束条件权重和为 1 def weight_sum_constraint(w): return np.sum(w) - 1 # 边界条件每个资产权重在 0 到 1 之间不允许做空 bounds [(0, 1) for _ in range(n_assets)] # 初始权重等权 initial_w np.ones(n_assets) / n_assets # 求解最小方差组合 opt_gmv minimize( portfolio_variance, initial_w, args(sigma,), methodSLSQP, boundsbounds, constraints{type: eq, fun: weight_sum_constraint}, ) gmv_weights opt_gmv.x gmv_vol np.sqrt(portfolio_variance(gmv_weights, sigma)) print(最小方差组合权重) for i, w in enumerate(gmv_weights): print(f {returns_df.columns[i]}: {w:.4f}) print(f组合年化波动率{gmv_vol:.4f})逻辑说明portfolio_variance接受权重w和协方差矩阵sigma返回的是组合的方差优化器通过调整w来让这个值最小化。weight_sum_constraint保证所有权重加起来严格等于 1这是“把钱全部投出去”的数学表达。bounds限制每个资产权重在 0 到 1 之间等价于不允许做空这也是国内多数资管产品的真实约束。SLSQP 方法即 Sequential Least Squares Programming支持等式和不等式约束适合这类中小规模的凸优化问题。资产数量在几十个以内时SLSQP 的速度和稳定性都够用。如果资产数量超过几百个需要换成专门的 QP 求解器如 cvxopt 或 OSQP这一点你在扩展时留意。3.3 画出有效前沿并标出各组合位置有效前沿的画法是在目标收益约束下反复求解最小方差问题把每个收益水平对应的最小方差收集起来连成一条曲线。import matplotlib.pyplot as plt # 在最小方差组合收益和最大收益资产之间取 50 个目标收益点 target_returns np.linspace(mu.min(), mu.max(), 50) frontier_vols [] frontier_rets [] for target in target_returns: constraints [ {type: eq, fun: weight_sum_constraint}, {type: eq, fun: lambda w, mumu, targettarget: w mu - target}, ] opt minimize( portfolio_variance, initial_w, args(sigma,), methodSLSQP, boundsbounds, constraintsconstraints, ) if opt.success: frontier_vols.append(np.sqrt(portfolio_variance(opt.x, sigma))) frontier_rets.append(target) plt.figure(figsize(8, 5)) plt.plot(frontier_vols, frontier_rets, b-, linewidth2, label有效前沿) plt.scatter([gmv_vol], [mu gmv_weights], colorred, s80, label最小方差组合) plt.xlabel(年化波动率) plt.ylabel(年化收益率) plt.title(有效前沿与最小方差组合) plt.legend() plt.grid(alpha0.3) plt.show()这里最值得注意的地方是lambda w, mumu, targettarget: w mu - target这个写法。由于target在循环里是变化的直接写lambda w: w mu - target会把所有循环里的约束都绑到最后一个target上这是闭包延迟绑定的经典坑。用mumu, targettarget作为默认参数把当前循环值固化进去才能保证每个收益点对应的是自己的约束。有效前沿的实际形状取决于你能接受的最低收益点在哪里。如果目标收益低于 GMV 的收益那么优化器给出的结果还是 GMV 本身这就是为什么前沿线的左端和 GMV 重叠的原因。金融含义是在目标收益低于最低可达收益时理性投资者不会选择承担更多风险。4. 历史收益率和协方差矩阵的估计误差是模型跑偏的最大原因均值-方差模型在理论上无懈可击但落地时最大的敌人不是模型本身而是输入参数的估计误差。直接用样本均值作为未来收益的预测用样本协方差矩阵作为未来风险的度量结果往往非常激进权重会在几个资产之间大幅跳转换一个样本区间结果变得面目全非。4.1 为什么样本协方差矩阵在资产数量多的时候会失效样本协方差矩阵的估计精度取决于观察期长度 T 和资产数量 n 的比值。当 n/T 接近甚至超过 1 时样本协方差矩阵会变得几乎奇异最大特征值和最小特征值之间的比值失真严重直接用它做优化模型会把最大的权重压在那些“看起来波动率最低但实际上是估计噪声”的资产上。看一个典型场景。股票加债券加商品的经典配置里资产数量通常 10 到 30 个日频数据取 252 天一年n/T 在 0.04 到 0.12 之间看起来不高。但问题在于资产之间收益率的相关性本身就比较高尤其在危机时期相关性上升会让有效样本量进一步缩水。4.2 Ledoit-Wolf 收缩压缩协方差矩阵的噪声Ledoit-Wolf 收缩的思想非常直接构造一个目标矩阵 F然后把样本协方差矩阵 S 和目标矩阵做一个凸组合得到收缩后的估计值。目标矩阵通常选择“所有资产方差相同、协方差为 0”的结构化矩阵收缩强度由一个参数控制这个参数取决于样本量和数据本身的噪声水平不需要你手动调。from sklearn.covariance import LedoitWolf # 用日频数据拟合收缩协方差矩阵 lw LedoitWolf() lw.fit(returns_df) shrinkage_coef lw.shrinkage_ sigma_shrunk lw.covariance_ * 252 sigma_sample returns_df.cov().values * 252 print(f收缩强度shrinkage{shrinkage_coef:.6f}) print(样本协方差矩阵对角线前 3 个, np.diag(sigma_sample)[:3].round(6)) print(收缩后协方差矩阵对角线前 3 个, np.diag(sigma_shrunk)[:3].round(6))对比观察可以发现收缩后的协方差矩阵对角线通常比原始样本值更大而非对角线元素被压缩。直觉是样本协方差里有些相关性是纯噪声完全相信它会把组合过于集中在“看起来低相关”的资产上收缩后相关性向全局均值回归权重就会更分散。提示sklearn.covariance.LedoitWolf基于的是多变量正态假设下的最优收缩强度公式。如果你用的是周频或月频数据直接把原始矩阵传入拟合即可*252这件事放在拟合之后做不要先年化再拟合否则收缩强度会被年化因子扭曲。4.3 预期收益率如何估计均值回归与风险溢价视角协方差矩阵可以靠收缩解决但预期收益率的估计没有这么成熟的统计工具。业界最常见的做法是用多因子模型或隐含收益率倒推但在个人投资者或中小团队场景下最可落地的思路是分层短期几个星期到几个月均值回归策略预期收益率接近零甚至为负因为短期价格波动主要是噪声。中期一到三年依赖资产自身的收益率分布特征可以用历史分位数设定乐观、中性、悲观三个情景分别做配置对比三个结果的重合区间。长期五年以上采用稳定的风险溢价假设股票 5% 到 7%长久期债券 2% 到 3%黄金贴近通胀。在代码实现的层面不要直接用mu returns_df.mean() * 252这种点估计作为预期收益。至少做一次简化版的稳健处理用近 3 年收益率的中位数替代均值或者干脆把 μ 统一设成相同值只看 GMV 组合的表现你会发现结果比任何花哨的收益预测都稳定。5. 滚动窗口回测验证模型真的有效而不是拟合过去模型算出来的权重再漂亮不做样本外验证都是纸上谈兵。回测的核心原则是防止前视偏差在 t 时刻做配置决策时只能用 t 时刻之前的数据不能用整个样本区间估计出的 μ 和 Σ。5.1 滚动回测的代码骨架设计一个滚动窗口回测每 60 个交易日重新估计一次协方差矩阵并计算 GMV 权重持有期间不调仓记录每日组合收益率。window 60 rebalance_freq 60 # 每 60 个交易日调仓一次 n_total len(returns_df) weights_history [] portfolio_returns [] for start in range(0, n_total - window, rebalance_freq): train returns_df.iloc[start : start window] test_start start window test_end min(test_start rebalance_freq, n_total) test returns_df.iloc[test_start:test_end] # 用训练段估计协方差矩阵 lw LedoitWolf() lw.fit(train) sigma_train lw.covariance_ * 252 # 不需要 mu直接用 GMV 权重 opt minimize( portfolio_variance, np.ones(n_assets) / n_assets, args(sigma_train,), methodSLSQP, bounds[(0, 1) for _ in range(n_assets)], constraints{type: eq, fun: weight_sum_constraint}, ) w opt.x # 持有期收益 权重 * 测试段每日收益的累计 period_returns test.values w portfolio_returns.extend(period_returns) weights_history.append(w) portfolio_returns np.array(portfolio_returns) # 回测结果评价 annual_ret np.mean(portfolio_returns) * 252 annual_vol np.std(portfolio_returns, ddof1) * np.sqrt(252) sharpe annual_ret / annual_vol if annual_vol 0 else 0 max_drawdown np.min(np.cumsum(portfolio_returns) - np.maximum.accumulate(np.cumsum(portfolio_returns))) print(f年化收益{annual_ret:.4f}) print(f年化波动{annual_vol:.4f}) print(f夏普比率{sharpe:.4f}) print(f最大回撤{max_drawdown:.4f})这段回测代码里有一个常见误差点test.values w算的是当日的组合收益率np.cumsum(portfolio_returns)是把收益率做累计加总当成净值曲线来算最大回撤。严格来说应该用np.cumprod(1 portfolio_returns)但对于日频收益率这种量级0.1% 到 1%两者的数值差异很小不影响回测结论的方向。回测结论要关注的核心不是累计收益有多高而是换一组时间窗口、换一个资产池结果是否还稳定。如果 GMV 组合的权重在每次调仓时资产分布都比较均匀说明协方差矩阵估计是可信的如果权重频繁地在两三个资产之间极端跳转说明收缩强度不够或者资产池里有高共线性资产。5.2 回测陷阱清单交易成本与调仓频率回测里最容易让人高估收益的因素就是忽略交易成本。每次调仓从旧权重变成新权重意味着买入和卖出产生手续费和冲击成本。你需要在每次调仓时计算换手率并扣减成本# 记录每次调仓的换手率 turnover_history [] prev_w np.zeros(n_assets) for w in weights_history: turnover np.sum(np.abs(w - prev_w)) turnover_history.append(turnover) prev_w w # 假设双边交易成本为 0.1% cost_rate 0.001 total_cost np.sum(turnover_history) * cost_rate net_annual_ret annual_ret - total_cost / (len(portfolio_returns) / 252) print(f总换手率{np.sum(turnover_history):.4f}) print(f扣除成本后年化收益{net_annual_ret:.4f})如果净值曲线显示模型的超额收益主要来源于低换手时的长期持有而高频调仓版本收益反而变差说明模型的 alpha 来自资产本身的风险溢价而不是择时能力。这种情况下应该主动降低调仓频率把 rebalance_freq 从 60 提升到 120 或 250减少无谓的成本损耗。6. 约束条件的实战应用把模型从论文变成能落地的方案前面用的约束只有权重和为 1 和非负两个但实际产品的约束远比这个复杂。部门经理甚至会直接给定“某一类资产的权重不能超过 30%”或者“组合对基准的跟踪误差不能超过 2%”。这一章把几个高频约束写成可执行的代码也指出哪些约束在数学上会改变问题的性质。6.1 单资产权重上限与行业中性约束加入单资产权重上限的方式非常直接修改 bounds 参数即可。但要注意把上限设得太紧会让有效前沿明显右移组合的可达风险下限被抬高这是约束的成本。行业中性约束适用于股票组合风格的资产配置要求组合在某个行业上的暴露与基准一致。它不是一个简单的权重上下界而是一个等式约束# 假设前两个资产同属某个行业需要行业权重不超过 35% industry_weights np.array([1, 1, 0, 0, 0]) # 标记前两个资产属于目标行业 constraints [ {type: eq, fun: weight_sum_constraint}, {type: ineq, fun: lambda w: 0.35 - w industry_weights}, ] opt_industry minimize( portfolio_variance, initial_w, args(sigma,), methodSLSQP, boundsbounds, constraintsconstraints, ) print(加入行业约束后的权重) for i, w in enumerate(opt_industry.x): print(f {returns_df.columns[i]}: {w:.4f})注意这里ineq表示不等式约束fun返回值必须大于等于 0 才表示约束满足。0.35 - w industry_weights 0即在数学上等价于行业权重不超过 35%。6.2 跟踪误差约束让组合不偏离基准太远跟踪误差约束写成数学形式是(w - w_b)^T Σ (w - w_b) TE^2其中 w_b 是基准权重TE 是年化跟踪误差目标。这个约束在 scipy.optimize 里不太好直接写因为它是一个二次不等式约束。SLSQP 对这类约束的支持不稳定更稳妥的做法是把它改写成目标函数的一部分用拉格朗日惩罚项处理# 基准权重等权或指数权重 benchmark_weights np.ones(n_assets) / n_assets def portfolio_variance_with_te(w, sigma, benchmark_weights, lambda_te1.0): tracking_error (w - benchmark_weights) sigma (w - benchmark_weights) return w sigma w lambda_te * tracking_error opt_te minimize( portfolio_variance_with_te, initial_w, args(sigma, benchmark_weights, 1.0), methodSLSQP, boundsbounds, constraints{type: eq, fun: weight_sum_constraint}, ) print(加入跟踪误差惩罚后的权重) for i, w in enumerate(opt_te.x): print(f {returns_df.columns[i]}: {w:.4f})lambda_te是惩罚系数越大代表组合越贴近基准但也会牺牲一定的收益潜力。实际使用时你可以循环测试几个不同的lambda_te值观察组合收益和跟踪误差之间的权衡曲线选在曲率拐点处的参数。最终给你的落地建议是先从最小方差组合加 Ledoit-Wolf 收缩起手把它作为基准组合然后叠加不超过三个业务约束每次只加一个约束对比约束加入前后的 GMV 权重变化、年化波动率和最大回撤。如果加约束后权重没有实质变化说明该约束是冗余的可以去掉。这套流程跑通后你就有了一个可以应对多数资产配置需求的最小可复现框架剩下的只是数据清洗和报告呈现的功夫。本文还有配套的精品资源点击获取