资讯动态

遗传算法投资组合选择:马科维茨与贝叶斯模型对比实验解析

发布时间:2026/10/18 10:09:09 来源:尧图企业网站定制
简介针对投资组合选择问题提供了一套基于遗传算法的完整对比方案覆盖马科维茨有效前沿与贝叶斯后验估计两种经典模型。资源面向金融工程、量化投资方向的学生与研究人员旨在通过实例说明不同算法在资产配置中的适用场景。包内共45个文件包括Python源码、Jupyter Notebook、Excel数据表格、模型对比可视化图、配套说明文档及演示幻灯片压缩包整体约12.23MB目录结构清晰便于直接查看与复现。已有178人学习下载。代码可完整运行遗传算法优化流程数据涵盖指数及申万行业样本图表展示了不同变异率、初始化方式、候选解数量等参数对优化结果的影响并结合模型对比图与描述性统计帮助读者深入理解三种方法的风险收益权衡与实现差异。无论是教学演示还是策略研究均具参考价值。1. 遗传算法做投资组合选择为什么要把马科维茨和贝叶斯拉来对比第一次解压 GA_portfolio 压缩包我注意到它和网上那些单文件遗传算法 demo 不一样里面有 py 脚本、ipynb 笔记本、三份 Excel 数据、二十多张实验对比图。遗传算法做投资组合选择的核心难点不在算法本身而在适应度怎么构造、约束怎么处理、参数怎么扫以及结果怎么证明比经典模型更有价值。这份资源把遗传算法、马科维茨均值-方差模型和贝叶斯模型放在同一批数据上做了完整对比每张图对应一个参数实验适合做课程设计、毕业论文实验段或想验证 GA 在资产配置里是否值得用的从业者。按数据对齐、模型对比再到避坑的顺序读py 脚本里的核心函数拆出来就能复用到自己的实验。2. 数据与模型基线先把马科维茨和贝叶斯的底子搭对2.1 三份 Excel 数据如何对齐与清洗资源里的数据分三层。指数数据1.xlsx 是行情主序列通常包含一个或多个指数的日收盘价申万行业.xlsx 是行业板块的收益率序列用来扩充资产池数据描述性统计.xlsx 是预先算好的均值、标准差、偏度峰度表拿来核对图表结论用。我拿到手的第一件事不是打开 GA 脚本而是先把这三份数据的日期索引对齐因为后面所有模型的协方差矩阵、均值向量都从这里来数据错了后面全是白跑。常见做法是直接用 pandas 按日期取交集再统一做 pct_change 换收益import pandas as pd import numpy as np # 指数行情与行业数据都以日期为索引 index_data pd.read_excel(指数数据1.xlsx, index_col0, parse_datesTrue) industry_data pd.read_excel(申万行业.xlsx, index_col0, parse_datesTrue) # 取交集并排序避免日期错位导致收益错算 common_dates index_data.index.intersection(industry_data.index) index_data index_data.loc[common_dates].sort_index() industry_data industry_data.loc[common_dates].sort_index() # 简单收益率第一行为 NaN 直接丢掉 returns index_data.pct_change().dropna().join( industry_data.pct_change().dropna(), howinner ) # 粗筛异常点超过 3 倍标准差先标出来看原因 zscore (returns - returns.mean()) / returns.std() print(shape:, returns.shape, | 异常单元格数:, (zscore.abs() 3).sum().sum())逻辑说明交集保证了收益率是同一个交易日算出来的不会把两段不同频率的数据硬拼在一起pct_change 之后第一行是 NaNdropna 顺手清掉。异常单元格统计不是用来直接删数据的而是提醒你先去看一眼那一天发生了什么——停牌、除权、数据录入错误都会造成尖峰直接影响协方差矩阵的估计。参数说明inner 是只保留两边都有的日期如果你的原始数据覆盖长度差别很大建议先打印两边的起止日期再决定要不要换 outer 补齐。对完日期后再看 data_corr.png 和描述性统计.png 两张图。data_corr 显示各资产之间的相关系数矩阵如果发现某些行业之间相关系数接近 0.9说明它们同涨同跌GA 搜索出的权重很容易在这组资产间反复横跳描述性统计则告诉你单资产的收益分布有多偏。这两张图是后面调参的依据不是装饰图。2.2 马科维茨均值-方差理论基线怎样落到可执行代码马科维茨模型的核心是有效前沿给定收益率目标求解使组合方差最小的权重。理论上漂亮落地就一句话——优化一个二次型目标函数。问题在于它要求输入精确的均值向量和协方差矩阵而这两样都来自历史估计估计误差会直接放大到最终权重上。这也是后面 GA 和贝叶斯模型存在的理由。最小实现用 scipy 的 SLSQP 求解器就能跑通from scipy.optimize import minimize def markowitz_weights(cov, mu, lam2.5): lam 是风险厌恶系数值越大权重越偏向低波动资产 n len(mu) def objective(w): return 0.5 * w cov w - lam * mu w constraints [{type: eq, fun: lambda w: w.sum() - 1}] bounds [(0.0, 1.0)] * n # 不允许做空单资产权重上限 1 result minimize(objective, np.ones(n) / n, methodSLSQP, boundsbounds, constraintsconstraints) return result.x if result.success else np.ones(n) / n逻辑说明目标函数里 0.5 是约定俗成的写法让梯度更干净lam 乘在期望收益前lam 越大风险惩罚越重。约束只写了权重求和等于 1 和 0 到 1 的边界没加单资产上限所以当某只资产历史收益特别突出时权重会直接顶到 1。参数说明初始解用等权向量SLSQP 对这类小规模问题足够稳但资产数超过 30 个之后建议改用带雅可比的目标函数或者直接换二次规划专用求解器。跑完马科维茨你会得到一组权重但 model_comparsion.png 里它的表现未必比 GA 好。原因很典型样本内均值-方差优化天生擅长拟合历史一旦切到样本外均值向量的估计误差全变成组合损失。记住这个结论后面对比实验才有意义。2.3 贝叶斯基线用 Dirichlet 先验把不确定性带进来贝叶斯模型在投资组合里的常见做法是把权重向量看成定义在单纯形上的分布而 Dirichlet 分布恰好是单纯形上的共轭先验。dirichlet.png 画的就是这个先验在不同 alpha 参数下的形态alpha 小的时候概率质量集中在单纯形的顶点附近意味着先验认为集中持仓更可能alpha 大的时候质量向中心聚拢接近等权。实现上不需要写完整的采样循环用后验均值就能出权重def bayes_dirichlet_weights(returns, alpha1.0): 把历史收益均值当成似然证据更新 Dirichlet 先验 n returns.shape[1] mean_ret returns.mean(axis0).clip(min0) # 负收益资产先给 0 证据 if mean_ret.sum() 1e-12: return np.ones(n) / n # 归一化成伪计数叠加到先验上 lik mean_ret / mean_ret.sum() posterior_alpha alpha lik * len(returns) return posterior_alpha / posterior_alpha.sum()逻辑说明alpha 是先验浓度决定后验在多大程度上相信历史数据。alpha 越大后验越接近等权模型越保守alpha 越小后验越追随历史收益容易重仓单个资产。lik * len(returns) 让证据强度跟样本量挂钩样本越多数据对后验的影响越大。参数说明clip(min0) 是把负收益资产的证据置零这在课程级实现里够用如果你的资产池里有做空逻辑这里得改成保留符号的版本。贝叶斯模型的价值在于它不假装自己知道真实分布而是把不确定性显式放进权重里。缺点也很实际alpha 怎么定是个选择题不同 alpha 出来的组合差异可能比模型之间的差异还大。所以对比实验里通常固定 alpha1保证基线公平。3. 遗传算法核心实现编码、适应度与参数地图3.1 权重编码与约束处理GA 处理投资组合问题第一步是决定染色体怎么编码。最常见也最省事的是实数向量编码染色体长度等于资产数量每一位基因对应该资产的权重。直接跑出的向量不一定合法因为基因值可能为负、可能不满足求和为 1所以需要一个解码函数把它映射到合法权重空间。我一般用截断加归一化的双保险def decode_weights(individual): 把任意实数染色体映射为合法权重向量 w np.clip(individual, 0.0, 1.0) # 禁止做空负权重截断为 0 total w.sum() if total 1e-12: return np.ones_like(w) / len(w) # 全零的兜底退回等权 return w / total # 归一化到求和为 1逻辑说明截断保证了权重非负归一化保证了求和为 1。全零兜底是必要的因为如果一条染色体的基因全是负数截断后总和会是 0直接除会报错。参数说明1e-12 这个阈值纯粹是数值保护你也可以用 np.isclose( )。需要提醒的是这种解码方式会让权重空间边界附近出现多对一——多个不同染色体可能解码出同一个权重向量所以种群的多样性要靠初始化方法补回来。3.2 适应度函数Sharpe 还是均值方差函数适应度函数是 GA 的指挥棒它决定进化方向。资源里的公式.png 画的就是适应度的数学定义。课程级实现里用夏普比率最省心因为它同时考虑了收益和风险对比马科维茨时也能直接对上话。实现时注意年化def sharpe_fitness(individual, returns, rf0.0, freq252): w decode_weights(individual) mu returns.mean(axis0) * freq # 年化均值收益 cov returns.cov() * freq # 年化协方差 port_ret mu w port_vol np.sqrt(w cov w 1e-12) # 加小量防止除零 if port_vol 1e-8: return -1e6 # 罚掉退化解 return (port_ret - rf) / port_vol逻辑说明freq252 是按交易日年化的常规取值如果你的数据是周频或月频要改成 52 或 12。port_vol 加 1e-12 防止全零权重算出零方差导致除零。返回 -1e6 的惩罚分支虽然少见但能避免 GA 钻空子找出一组方差为 0 的退化解。参数说明rf 默认 0对比实验里对所有模型用同一个 rf 即可不影响相对排序。3.3 选择、交叉、变异的实现细节GA 的三个算子直接决定收敛质量。资源的 muate_method 系列图说明代码里至少比较了两种变异方式。拆包后整理出的标准实现是锦标赛选择加均匀交叉import numpy as np def tournament_select(pop, fitness, k3): 锦标赛选择随机抽 k 个挑适应度最高的 idx np.random.choice(len(pop), sizek, replaceFalse) best max(idx, keylambda i: fitness[i]) return pop[best].copy() def uniform_crossover(parent_a, parent_b): 均匀交叉每个基因位独立决定来自哪个父本 mask np.random.rand(len(parent_a)) 0.5 return np.where(mask, parent_a, parent_b) def mutate(individual, rate0.1, methodgaussian, sigma0.1): 两种变异gaussian 局部扰动 / uniform 随机重置 m individual.copy() for i in range(len(m)): if np.random.rand() rate: if method gaussian: m[i] np.random.normal(0, sigma) else: # uniform reset m[i] np.random.rand() return m逻辑说明tournament 的 k 控制选择压力k 越大高适应度个体越容易当选收敛越快但多样性掉得也快k 太小则收敛慢。uniform_crossover 对实数编码实现简单、不产生越界值但会破坏基因之间的相关性——权重是耦合的两个基因位被拆散后解码可能和两个父本都差很远。参数说明gaussian 变异适合后期微调sigma0.1 表示每个基因位有 10% 概率在原位加一个标准差为 0.1 的扰动uniform 变异适合前期逃出局部最优因为它是完全随机重置。3.4 参数地图变异率、候选数、迭代数的扫参逻辑资源里二十多张图的命名直接暴露了作者做的参数实验。把图名和参数的对应关系整理成一张表照着扫就不容易漏参数对应图文件常见扫参范围影响变异概率 ratemuate_rate_mean.png / muate_rate_std.png0.01 0.30多样性 vs 收敛速度初始候选数 num_candidatesnum_candidates_mean.png / _std.png20 100初始覆盖范围迭代代数 num_iternum_iter_mean.png / _std.png50 300搜索深度初始化方法 init_methodinit_method_mean.png / _std.pngrandom / equal起始解分布变异方式 muate_methodmuate_method_mean.png / _std.pnggaussian / uniform局部搜索强度训练期 / 持有期Tperiod.png按样本长度切样本外适用性主循环把上面几个函数串起来就是一个能跑的 GAdef run_ga(returns, num_iter150, num_candidates60, mutation_rate0.1, methodgaussian): n returns.shape[1] pop [np.random.rand(n) for _ in range(num_candidates)] fitness [sharpe_fitness(ind, returns) for ind in pop] for _ in range(num_iter): new_pop [] while len(new_pop) num_candidates: a tournament_select(pop, fitness, k3) b tournament_select(pop, fitness, k3) child uniform_crossover(a, b) child mutate(child, mutation_rate, method) new_pop.append(child) pop new_pop fitness [sharpe_fitness(ind, returns) for ind in pop] best_idx int(np.argmax(fitness)) return pop[best_idx]逻辑说明每一代都完全重建种群子代数量保持和父代一致这是标准的代际模型。注意种群重建时没有保留精英理论上最优个体可能被交叉变异破坏所以实验里最好在循环外部单独记录历史最优染色体。参数说明num_candidates 和 num_iter 的乘积就是总的适应度评估次数扫参时保持评估次数一致不同参数组合之间才公平。扫参顺序我习惯先粗扫再细扫先把 num_candidates 和 num_iter 定在中间值比如 60 和 150只扫变异率找到合理区间后再固定变异率扫候选数和迭代数。每张 mean 图旁边都配了一张 std 图这是整份资源里最值得学的设计下一章细讲。4. 对比实验与结果解读model_comparsion 和标准差图里藏的信息4.1 model_comparsion.png 与 std_comparsion.png三模型同场竞技model_comparsion.png 是整份资源的门面图对比的是遗传算法、马科维茨模型和贝叶斯模型在同样数据上的表现。读图时先看纵轴指标——通常是夏普比率或累计收益再看横轴是模型还是不同时间段。多数实现的画法是三个模型各算一组权重用同一段样本外数据回测画成柱状图。如果 GA 的柱子比马科维茨高别急着下结论先确认一个前提马科维茨用的是样本内估计的均值和协方差还是在样本外滚动更新的。两种做法得到的结论完全相反前者必然高估马科维茨后者才算公平。std_comparsion.png 配合着看模型稳定性。GA 因为带随机性每次跑的结果不同标准差反映的是多次重复实验的离散程度。三个模型里马科维茨确定性最高贝叶斯次之GA 往往最不稳——这不是缺陷而是随机搜索的代价。所以对比结论不要只写GA 收益更高要写成GA 收益较高但波动更大马科维茨胜在稳定贝叶斯居中这类完整表述。4.2 mean 和 std 图成对看别被最优值欺骗这份资源最值得抄的写法是每个参数实验都配 mean 和 std 两张图。muate_method_mean.png 看平均最优适应度muate_method_std.png 看这个平均值的波动。只盯 mean 是新手最容易犯的错误也是最典型的翻车现场某个变异率在 mean 图上排第一但 std 图显示它的方差比倒数第一还大说明这个结果纯靠运气换个随机种子结论就变了。正确读法是画一条均值-标准差的权衡线。比如扫变异率时0.05 和 0.15 的 mean 差不多但 0.05 的 std 明显小那我就会选 0.05因为它稳定且不差反过来如果 0.15 的 mean 高出一截而 std 只是略大那可以保留 0.15 并缩小候选数再验证。这个选参逻辑和投资组合本身的收益-风险权衡是同构的理解这一点参数就不全是黑匣子了。4.3 data_corr.png 与描述性统计先看数据再决定参数data_corr.png 的相关系数矩阵直接影响 GA 的搜索难度。如果资产间相关性普遍在 0.7 以上有效前沿会变得很平坦适应度景观上有无数个接近最优的解GA 很容易早熟——这时应该调高变异率或换成 uniform 变异。反过来相关性低说明资产间独立性好适应度景观有清晰的山峰用 gaussian 变异加低变异率更容易爬上山头。描述性统计.png 告诉你的则是收益分布的形状。高偏度的资产会让适应度函数出现极端值一条染色体的夏普可能因为某一天的暴涨而虚高GA 会把这种虚假优势放大到整个种群。我一般会在算均值向量之前看一眼最大单日收益如果某资产单日收益超过 20%先把那一天单独标注再决定是缩尾处理还是直接剔除。这不是数据造假而是不让单点噪声主导全局搜索。4.4 s_average_vs_random 是底线测试GA 至少要比随机强s_average_vs_random.png 容易被忽略但它其实是整份资源里最该先看的底线测试。它对比的是 GA 的搜索结果和两路基准一是随机权重组合的平均表现二是等权组合。GA 如果连这两个基准都跑不赢后面所有对比都没有意义——不是 GA 不行而是适应度或者约束处理有问题。等权组合在大部分资产上都算是个强基准GA 至少应该稳定超过它一截才算合格。这张图的另一个价值是把GA 是否只是运气好这个问题前置化随机权重在足够多次抽样下也能碰到不错的解GA 的意义在于用更少的评估次数找到它。跑实验时我会先跑这个对比确认过了再继续省得后面一堆参数扫描全白做。Tperiod.png 管的是时间窗口切分训练期用多长、持有期用多长直接决定实验结论的可信度。常见做法是训练期 120 到 250 个交易日持有期 20 到 60 个交易日对应月度到季度的再平衡频率。窗口太短协方差估计全是噪声窗口太长训练期里的市场结构早就变了。经验是固定训练期去扫持有期看不同持有期下 GA 与马科维茨的差距是否稳定——如果 GA 只在某一个持有期下赢、其他都输那结论基本是过拟合出来的。5. 避坑手册GA 投资组合的 5 条血泪经验5.1 迭代几百代适应度纹丝不动种群提前收敛现象跑 num_iter200 的实验前 30 代适应度涨得不错之后 170 代完全平着走多次重复的 std 图方差接近 0每个种子跑出来的都是同一个解。原因锦标赛选择的 k 太大加上变异率太低高适应度个体快速霸占种群变异又不足以打破同质化种群在早期就失去了多样性。另一个常见源头是初始化方法如果只用等权初始化所有个体挤在同一个区域后代再怎么交叉也在原地打转。解决先把变异率从 0.01 提到 0.1 以上或者把变异方式从 gaussian 换成 uniform 重置同时把锦标赛 k 从 3 降到 2减少选择压力。检查 init_method 是否覆盖了权重空间的不同区域。我一般会在适应度评估时顺手记录种群基因的方差连续 20 代低于阈值就提前终止并报警。5.2 权重全部压到单只资产上现象解码出的权重向量接近 one-hot90% 以上的仓位在一只资产上组合夏普看起来还挺高但样本外回测一塌糊涂。原因适应度函数只惩罚总方差没限制单资产集中度。历史数据里某只资产恰好是幸存者均值很高、方差被样本期掩盖GA 自然把它当成最优解。解决在 decode_weights 后面再加一道限制给单资产权重设上限常见做法是 0.3 或 0.4或者在适应度函数里加一个集中度惩罚项组合权重熵值低于阈值就扣分。改完再重跑参数扫描你会发现最优参数区间整个发生了移动。5.3 指数和行业数据日期对不齐结果莫名漂移现象数据折线图看起来挺正常但协方差矩阵算出来某些资产之间相关系数异常模型对比图里马科维茨的权重在不同运行之间大幅跳动。原因两份 Excel 的日期覆盖范围不一致行业数据比指数数据多了一段没有成交的日期直接 join 会让很多行变成 NaNpandas 默认跳过但跳过的位置不对齐导致收益率序列错位。解决在数据对齐代码里加一步打印两个文件的起止日期和行数确认交集占比。交集占比小于 80% 说明数据源本身有问题优先回查源头而不是硬拼。我习惯把对齐后的数据单独存一份 parquet所有模型共用避免每次跑实验都对一遍日期。5.4 随机种子不固定两次实验结论完全相反现象同一组参数早上跑和下午跑model_comparsion 图里 GA 的柱子忽高忽低有一次比马科维茨高 20%另一次反而低 10%。原因GA 的初始化、选择、交叉、变异全依赖 np.random 的全局状态没固定种子就等于每次从不同的起点搜索。这不算 bug但对比实验必须控制变量。解决py 脚本入口和 ipynb 第一个 cell 都写 np.random.seed(42)并且对每个参数组合跑 10 到 20 次重复实验用 mean 和 std 图里的统计量做结论。固定种子只能保证单次可复现真正可信的结论必须来自多次重复。5.5 只盯着 mean 图选参数样本外直接翻车现象扫多组参数每组在训练集上算适应度选 mean 最高的一组拿到样本外回测却比倒数第二的还差。原因训练集上的适应度高不代表样本外好GA 很容易在训练期过拟合——它找到的是在历史数据上恰好表现好的权重组合而不是结构上稳健的组合。解决把参数选择改成训练期选参加验证期确认的两段式流程训练期扫参数挑出 mean 和 std 都合理的候选再用一段没参与进化的验证期做最终确认。Tperiod.png 的实验设计已经暗示了这个思路照做就行。6. 从静态到动态single_vs_dynamic 图和滚动再平衡验证single_vs_dynamic.png 是这份资源里最有实用价值的一张图。它对比的是一次性算好权重然后一直持有和每隔一段时间用 GA 重新优化两种策略。静态方案的好处是省事坏处是市场结构一变化当初的最优权重就变成了次优动态方案每次用最近的数据重新进化代价是计算量翻好几倍。图里动态曲线的优势其实也取决于再平衡频率和交易成本A 股印花税加佣金每两个月调一次还能接受每两周调一次就未必划算。验证动态策略最直接的方法是滚动窗口回测。训练窗取 120 到 250 日步长等于持有期常见是 20 或 60 日def rolling_ga_backtest(returns, window180, step60, **ga_params): 滚动窗口回测每 step 天重新跑一次 GA 并计算持有期收益 weights_history, perf [], [] for start in range(0, len(returns) - window, step): train returns.iloc[start:start window] test returns.iloc[start window:start window step] if len(test) 0: break best_individual run_ga(train, **ga_params) # 返回最优染色体 w decode_weights(best_individual) weights_history.append(w) period_return (1 test.values w).prod() - 1 perf.append(period_return) total np.prod([1 p for p in perf]) - 1 return weights_history, perf, total逻辑说明每次进化只用训练窗内的数据持有期完全样本外这是验证 GA 是否真正学到结构而不是背数据的关键。weights_history 可以用来画权重随时间的变化如果某个资产权重从 30% 跳到 0说明那段时间它被数据淘汰了这是动态再平衡的正常现象。参数说明window 和 step 的比例一般保持在 3 到 6 之间window 太短样本不足太长反应迟钝ga_params 里记得把 num_iter 调低一些因为滚动场景要跑很多次单次实验没必要用 300 代。提示滚动回测里每次 run_ga 都固定同一个随机种子否则同一段训练窗每次跑出的权重不一样回测曲线会有大量毛刺没法判断策略本身的稳定性。从那以后我每次跑这类对比实验都强制走一遍固定流程先固定随机种子再对齐数据看描述性统计然后跑 s_average_vs_random 的底线测试确认 GA 至少比随机强接着粗扫加细扫参数mean 和 std 成对看最后用滚动窗口做样本外验证。五步缺一步结论都可能翻车。这份资源的图给你铺好了路照着走能省下几个星期的试错时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑