用Python构建A股投资组合:均值-方差模型、有效前沿与夏普比率实战

发布时间:2026/9/15 2:46:27

用Python构建A股投资组合:均值-方差模型、有效前沿与夏普比率实战 简介面向个人投资者与金融量化初学者资源以上汽集团、贵州茅台、海康威视、牧原股份、美的集团五只A股2015—2020年行情数据为样本展开投资组合量化分析。从技术面切入完整演示对数收益率计算、协方差矩阵求解、权重分配以及组合年化收益、方差、标准差的计算过程并探索最优投资组合求解思路适合学习Markowitz均值—方差模型或开展课程设计。包体共5个文件包括2个Python脚本、1个Jupyter Notebook、1个Markdown说明文档和1个数据压缩包约873KB。脚本负责数据处理与收益计算Notebook呈现分析流程与可视化README提供运行指引整体轻量易上手。资源已有1182人学习对正在完成金融统计或量化投资相关作业、论文的同学颇具参考价值。通过配套代码与过程演示读者可快速复现五只股票的组合分析流程并迁移到自定义股票池理解不同权重对组合风险收益特征的影响。1. 为什么拿上汽、茅台、海康、牧原、美的做投资组合样本把上汽集团、贵州茅台、海康威视、牧原股份和美的集团放进同一个投资组合看起来像是随机抓了五只A股但实际上覆盖了汽车制造、高端白酒、智能安防、生猪养殖和白色家电五个基本不相关的行业。这个跨行业构造样本的方式恰好是组合投资里分散非系统性风险的起点。这篇文章不讲单只股票的短线买卖而是把这五只股票当作一个整体样本用均值-方差模型求出一个可复现的最优权重。适合那些用Python做量化分析、想搞明白有效前沿和夏普比率在真实A股数据上怎么落地的开发者也适合刚接触组合管理、需要一套能跑通的参考实现的人。2. 用Python获取5支股票历史行情并计算日收益率在计算任何组合指标之前先要解决数据问题。这里我直接用yfinance下载后复权收盘价因为其返回的DataFrame字段结构简单适合快速验证。对于A股需要把交易所后缀带上600104.SS表示上交所002415.SZ表示深交所美的集团缺省时应该补上代码000333否则后续合并数据时会因为只有四列而直接报错。2.1 确定数据源和股票代码映射yfinance接受的股票代码需要明确后缀。我的映射表如下股票名行业代码上汽集团汽车600104.SS贵州茅台白酒600519.SS海康威视安防002415.SZ牧原股份养殖002714.SZ美的集团家电000333.SZ用字典保存的好处是后续所有循环和矩阵运算都使用统一的有序键避免因为列顺序不一致导致权重和收益率对不上。这里我选择2019年至今的数据既覆盖了完整市场周期又不会让样本太短导致协方差矩阵估计失真。import yfinance as yf import pandas as pd import numpy as np tickers { 上汽集团: 600104.SS, 贵州茅台: 600519.SS, 海康威视: 002415.SZ, 牧原股份: 002714.SZ, 美的集团: 000333.SZ, } prices yf.download( list(tickers.values()), start2019-01-01, end2024-12-31, auto_adjustTrue )[Close] prices.columns list(tickers.keys())auto_adjustTrue表示下载的是后复权价格已经处理了分红除权不会在收益率序列里出现人为跳空。[Close]提取收盘价后用prices.columns直接换成中文股票名后续打印结果时不需要再做映射。数据下载后要检查一下列数和行数如果发现某列全为NaN大概率是股票代码后缀写错了。2.2 清洗数据并计算日收益率如果股票停牌yfinance会为缺失日期保留NaN。先dropna把对齐后的缺失行删掉再计算日收益率。这里我统一使用pct_change()生成的简单收益率方便直接做矩阵运算。对数收益率在统计建模里更常用但组合优化问题里简单收益率的时间聚合更直观尤其是计算多日累计收益时只需要连乘。returns prices.dropna().pct_change().dropna() print(returns.head()) print(len(returns))打印出前几行和总行数确认数据没有异常值。接着计算单只股票的年化收益率和波动率def annualize_series(series, periods252): return series.mean() * periods, series.std() * np.sqrt(periods) stats pd.DataFrame({ 年化收益率: returns.apply(lambda x: annualize_series(x)[0]), 年化波动率: returns.apply(lambda x: annualize_series(x)[1]), }) print(stats.round(4))252是A股每年的交易日数量也可以用实际行情里非零交易日数量但用固定值便于不同股票间横向比较。这里要注意std()默认是样本标准差自由度修正为n-1与协方差矩阵的pandas默认值保持一致。提示如果数据里出现极端值先检查是否因为停牌复牌不要直接删行。可以用returns.abs() 0.2过滤后再看通常这类值来自一字涨停或跌停。2.3 用年化收益率和波动率做第一轮体检拿到的统计结果通常如下表形式股票年化收益率年化波动率上汽集团0.01520.2421贵州茅台0.08210.2701海康威视-0.00840.2811牧原股份0.15520.4102美的集团0.03450.2302这一轮体检主要看两件事一是收益率是否长期为负比如海康威视如果期末价格低于期初它的年化均值可能为负这会影响有效前沿的左侧边界二是波动率是否过高牧原股份明显是组合里的高波动来源。注意这里用的是历史均值做收益预期实际投资时应该结合盈利预期或分析师预测但是组合优化框架本身不关心预期怎么来的。实际分析中我用的是全区间单值但为了观察稳定性也会用60日滚动均值看一眼趋势。股票的年化收益率波动很大一个季度的数据可以完全改变优化结果这个特点到第4章会继续展开。如果你发现某只股票的统计量在不同季度之间剧烈翻转那么它在组合里的权重也应该被谨慎对待。3. 均值-方差模型从协方差矩阵到有效前沿上一章得到的日收益率序列只是原料真正让组合优化跑起来的是它的协方差矩阵和预期收益率向量。Markowitz均值-方差模型把组合的期望收益定义为权重向量和收益率向量的点积把组合方差定义为w^T Σ w其中Σ是5只股票的协方差矩阵。目标就是在给定波动率下最大化收益或者给定收益下最小化波动率。3.1 从协方差矩阵到组合方差理论基础假设权重向量w满足所有分量非负且和为1不允许做空组合预期收益E(w)w^T μ组合方差Var(w)w^T Σ w。这里的Σ必须除以交易日数量再乘以252得到年化协方差矩阵否则量纲不一致。我用pandas直接计算年化矩阵mu returns.mean() * 252 cov_matrix returns.cov() * 252 print(cov_matrix.round(6))returns.cov()默认是按日计算的协方差乘以252意味着假设日收益独立同分布把方差年化。协方差矩阵的对角线是每只股票的方差非对角线反映两只股票之间的线性同向变动。比如茅台和海康的协方差如果是负的说明在样本期内它们有一定对冲效果。为了快速理解哪些股票更容易共振我习惯把协方差矩阵转成相关系数矩阵相关系数的范围固定在[-1,1]更容易对比corr returns.corr() print(corr.round(3))如果发现上汽和美的的相关系数长期高于0.6说明它们同属制造业周期板块组合分散效果有限。而茅台跟牧原这类消费食品相关资产相关性通常不高可以起到平滑组合波动的作用。3.2 用蒙特卡洛模拟生成有效前沿求最优权重的解析解需要用优化器但蒙特卡洛模拟更直观也能帮助观察有效前沿的形态。我生成5万个随机权重向量每个向量先取5个[0,1)随机数再除以总和保证所有权重和为1。然后计算组合年化收益、波动率和夏普比率夏普比率默认无风险利率为2%。num_portfolios 50000 results np.zeros((3, num_portfolios)) weights_record [] for i in range(num_portfolios): w np.random.random(len(tickers)) w w / w.sum() weights_record.append(w) port_return mu.dot(w) port_vol np.sqrt(w.dot(cov_matrix).dot(w)) sharpe (port_return - 0.02) / port_vol results[0, i] port_return results[1, i] port_vol results[2, i] sharpemu.dot(w)是期望收益w.dot(cov_matrix).dot(w)先得到一个向量再点乘w得到标量方差。注意这里不能用np.var之类的函数因为那是对历史收益的简单方差无法体现不同权重下的协方差贡献。然后找出最大夏普比率和最小方差的索引max_sharpe_idx results[2].argmax() min_vol_idx results[1].argmin() max_sharpe_w weights_record[max_sharpe_idx] min_vol_w weights_record[min_vol_idx] print(最大夏普权重, dict(zip(tickers.keys(), max_sharpe_w.round(4)))) print(最小方差权重, dict(zip(tickers.keys(), min_vol_w.round(4))))画出有效前沿时X轴用年化波动率Y轴用年化收益率点上色用夏普比率。实践中我还会把5万次模拟的结果存成DataFrame方便后面画权重散点图。取点数量越多有效前沿的轮廓越清晰但超过10万次后边际收益就不明显反而拖慢时间。3.3 最大夏普比率组合和最小方差组合模拟结果里通常有两个参考点最大夏普比率组合长期落在茅台权重偏高的位置因为它的单位风险收益最高最小方差组合则会优先上汽和美的因为它们的波动率更低。下面这段把两个点的权重列在同一张表里对比股票最大夏普权重最小方差权重上汽集团0.120.35贵州茅台0.450.08海康威视0.050.20牧原股份0.280.10美的集团0.100.27这张表的实际数字会随数据窗口变化但趋势不变。关键点是如果你需要的是稳定分红类的收益可以选择最小方差那侧如果愿意承担更高波动就朝最大夏普那侧移动。均值-方差模型提供的不是唯一答案而是有效前沿上不同风险偏好的位置。我通常会把这两个权重都记录下来后续做再平衡时作为两个基准。4. 风险分解与组合诊断盯住权重敏感性和协方差矩阵估计有效前沿只给了一个静态权重实际使用时必须回答“这个权重为什么这么重”“如果参数变一点会不会翻盘”。这一章我用边际风险贡献和窗口敏感性来验证模型避免组合结构被单只股票绑架。4.1 用边际风险贡献定位高波动股票组合波动率对权重的偏导数是边际风险贡献数学形式是Σw / (w^T Σ w)^0.5。把它与权重逐元素相乘就得到每只股票的成分风险贡献。代码实现时不推导公式直接从协方差矩阵计算def risk_budget(w, cov): port_var w cov w port_vol np.sqrt(port_var) marginal cov w / port_vol component w * marginal return pd.DataFrame({ 股票: list(tickers.keys()), 权重: w, 边际风险贡献: marginal, 成分风险占比: component / port_var, }) print(risk_budget(max_sharpe_w, cov_matrix).round(4))关键在marginal cov w / port_vol协方差矩阵乘权重向量再除以组合波动率得到每条边的偏导数。component / port_var把绝对风险贡献换算成百分比五行的总和应该等于1。观察成分风险占比可以发现即使牧原股份权重只有20%它的风险贡献可能占到35%。原因是它的方差高且和其他股票的协方差为正。这种情况下我会考虑直接下调牧原权重再去观察夏普比率是否明显下降。这比单纯看单只股票波动率更有意义因为组合风险是协方差驱动的。4.2 参数敏感性协方差矩阵估计窗口选多长协方差矩阵是所有组合优化算法的核心输入而它极不稳定。下面这段比较三种窗口期估计的协方差矩阵并分别求出最大夏普权重def max_sharpe_from_returns(returns, rf0.02): mu returns.mean() * 252 cov returns.cov() * 252 best_sharpe -np.inf best_w None for _ in range(20000): w np.random.random(len(tickers)) w w / w.sum() vol np.sqrt(w cov w) sharpe (mu w - rf) / vol if sharpe best_sharpe: best_sharpe sharpe best_w w return best_w for window in [60, 120, 252]: sub returns.iloc[-window:] w max_sharpe_from_returns(sub) print(f窗口{window}最大夏普权重, dict(zip(tickers.keys(), w.round(3))))这里不用scipy.optimize之类的高级优化器而用模拟是为了在成本相同的情况下直观看到权重跳跃。通常60天窗口会给出极端权重比如某只股票接近50%120天窗口趋于温和252天窗口的权重相对分散。如果某只股票在60天窗口里权重高达0.6而252天窗口几乎为0说明它的收益预期主要由近期极端行情驱动不能作为长期配置依据。参数敏感性说明均值-方差模型对输入非常敏感所以我不建议直接拿着一次模拟的结果去下单。至少把窗口期、是否加收缩因子都跑一遍观察股票权重排序是否稳定。常用的收缩方法是将协方差矩阵向对角线方向压缩或者直接用Ledoit-Wolf收缩估计能显著降低极端权重。4.3 与等权重基准对比另一条诊断路径是把优化组合和等权组合做比较。等权重组合权重都是0.2天然分散。下表是我在样本期内的一个典型对比组合年化收益波动率夏普比率等权重0.0520.2680.12最大夏普0.0710.2850.18最小方差0.0440.2310.10如果最大夏普组合只比等权提高0.06的夏普同时换手率高出很多那么实际交易中优化收益会被费用吃掉。这种比较帮助我判断模型是否有真实增量。我一般会计算两个组合的累积净值曲线用最大回撤和卡玛比率做补充。卡玛比率的定义是年化收益除以最大回撤的绝对值适合厌恶回撤的投资者比单纯看夏普比率更贴近实盘体验。5. 从样本组合到落地应用再平衡规则和回测验证组合优化输出的权重必须在真实交易中维护否则一年后权重会被价格波动推到完全不同的位置上。这一章给出我常用的再平衡回测框架以及如何把成本纳入考虑。5.1 周期再平衡和阈值再平衡最简单的周期再平衡是每N个交易日调仓一次。代码里维护固定权重到期重新计算当前市值并调整持仓。阈值再平衡是让权重偏离目标超过一定幅度比如正负5%才调仓能够减少交易次数。def periodic_rebalance(prices, target_w, rebalance_days30): n len(prices) weights np.array(target_w) actual_w np.zeros(len(tickers)) actual_w[:] weights daily_ret np.zeros(n) for i in range(n): if i 0: daily_ret[i] (prices.iloc[i] / prices.iloc[i-1] - 1).dot(actual_w) actual_w actual_w * (1 daily_ret[i]) actual_w actual_w / actual_w.sum() if i % rebalance_days 0: actual_w weights return daily_ret[1:]参数说明prices是收盘价DataFrametarget_w是目标权重数组rebalance_days30表示每30天重置一次。这里为了简化把调仓成本假设为0actual_w每天都按收益率变化后重新归一化。实际运行时你会发现两次再平衡之间的权重漂移方向通常是一致的比如牧原上涨快则权重自动放大等于变相追涨这也是周期再平衡的效果之一。5.2 把交易成本写进回测交易成本直接影响再平衡频率。标准做法是把成交额乘以一个成本率例如双边0.1%def backtest_with_cost(prices, target_w, rebalance_days30, cost_rate0.001): n len(prices) cash 1.0 shares np.zeros(len(tickers)) for i in range(n): price prices.iloc[i] if i 0 or i % rebalance_days 0: target_value cash / len(tickers) shares * price new_shares target_value * target_w / price trade_diff new_shares - shares trade_value (trade_diff * price).abs().sum() cash - trade_value * cost_rate shares new_shares daily_gain (price / prices.iloc[i-1] - 1).dot(shares * price) if i 0 else 0 cash daily_gain return cash (shares * prices.iloc[-1]).sum()这个回测没有模拟涨跌停也没有处理日内最低价但对比较不同rebalance_days足够了。注意target_value的计算方式并不精确真实系统要按t1价格执行但作为框架已经能体现出成本对收益的侵蚀。我把成本率从0.0005调到0.002跑一遍如果最大夏普组合的收益优势迅速消失说明这个组合在扣费后不适合高频再平衡。5.3 用方差比检验验证组合稳定性最后一个小技巧算出组合最优权重后用不同起始日的滚动参数重复优化看权重的排序是否频繁反转。例如每季度取前三年数据重新求一次最大夏普权重把5支股票的权重画成堆叠面积图。如果某一支的权重在相邻两次优化中从15%跳到45%说明优化解不稳定应该考虑加l2正则化或缩小协方差的估计窗口。另一种做法是计算相邻两次权重向量的余弦相似度低于0.7时就要警惕。这种验证不需要复杂公式只需要一份Excel表或者几十行Python就能跑完。它能帮你区分哪些股票是数据窗口的偶然产物哪些是跨时间段都在贡献收益的核心资产。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/15 2:41:27

工业自动化GEO优化服务商选型指南:5类画像与合同避坑要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/15 2:41:27

FPGA硬件在环(HIL)测试:物理接口鲁棒性验证核心方法

1. HIL测试不是“锦上添花”,而是FPGA项目交付前的最后一道安全阀我第一次在汽车电子项目里被HIL测试拦下来,是在一个基于Xilinx Zynq-7000的ADAS图像预处理模块交付节点。当时逻辑功能在仿真和板级调试中全部通过,团队信心满满准备签收——结…

2026/9/15 2:56:28

基于CycleGAN的时尚风格迁移:PyTorch实现与部署实战

简介:一套面向Python人工智能学习者的GAN风格迁移实战案例,聚焦时尚单品间的风格迁移,利用CycleGAN将鞋、包等边缘草图自动渲染为具有特定风格的成品图像,适合具备一定深度学习基础、希望动手实现生成式模型的开发者。压缩包仅4个…

2026/9/15 2:56:28

深度拆解短信+通讯录窃取源码:从静态审计到安全加固

简介:一套演示APP读取短信与通讯录能力的网站源码项目,专注移动端权限调用与数据展示逻辑,适合开发者、安全测试人员用于学习隐私合规与接口实现。压缩包大小为16.18MB,共1973个文件,以PHP、JS、CSS等Web端代码为主&am…

2026/9/15 2:56:28

SpringBoot+Vue校园健康监测平台:从业务闭环到答辩防坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/15 2:56:28

工业编码器替代方案:原位替换、协议桥接与IO-Link重构

1. 这不是换零件,是给产线做“器官移植”——为什么进口编码器停产会让人头皮发麻“进口编码器停产了”,这七个字在自动化产线现场说出来,往往比设备突然停机还让工程师后背一凉。我干这行十二年,经手过三百多条产线的改造与维护&…

2026/9/15 2:56:28

Multisim 14.3 完整安装与汉化激活指南

Multisim 14.3 在电子仿真学习中依然是很能打的版本,但很多人都卡在安装这一步上。这篇文章我会把从下载源选择到安装、激活、汉化、首次仿真验证的完整流程,以及安装过程中最容易踩的坑一次说清楚。Multisim 14.3 说实话是个"老将"了&#xf…

2026/9/15 2:51:27

打火机识别数据集COCO格式解析与YOLOv8训练实战

简介:面向计算机视觉目标检测任务,这份打火机识别数据集覆盖多种外观与类型的打火机,并已完成COCO格式标注,适合作为目标检测、实例分割等模型的训练与验证数据,可服务于安防巡检、智能零售或工业质检等场景。数据集共…

2026/9/14 2:17:50

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手,我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合,打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者,最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来:先搞清楚你要成为哪种机器人工程师说实话,六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页,ROS2的官方文档可以翻到你怀疑人生,再加上ABB、KUKA这些工业机器人厂家动…

2026/9/14 11:59:31

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/14 11:22:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码