从Markowitz到遗传算法:Python实现投资组合优化的完整实战 简介面向投资组合优化场景的Python实战资料包围绕Markowitz均值-方差模型与遗传算法两条主线展开适合量化交易、金融数据分析、风险管理方向的开发者作为从理论到落地的参考。压缩包共59个文件约18.68MB包含28个Python脚本、17个PDF文档、4个CSV数据文件、6个URL链接和1个license等辅助文件py脚本构成核心可运行代码PDF与url提供理论说明与扩展阅读CSV提供可直接用于模型训练的价格数据。目前已有1218人学习下载。工程内封装了资产收益率计算、协方差矩阵构建、最小方差组合求解等基础函数并给出基于DEAP的遗传算法优化流程支持自定义评估函数与约束结合数据文件和说明文档读者可快速复现Markowitz有效边界还可进一步对比智能优化与传统方法的差异适合深度学习或改造为自己的量化实验框架。1. 投资组合优化从 Markowitz 到遗传算法为什么你的组合总差一口气做量化交易的头两年我一直在用 Markowitz 均值-方差模型给资金分配权重回测曲线漂亮得能当壁纸一上实盘就变脸。后来仔细一查不是我参数调错了而是这个模型在真实约束面前根本不成立它假设收益服从正态分布、不支持整数手数、不考虑交易成本更处理不了最多持有 10 只股票这类基数约束。于是我把目光转向遗传算法GA用 Python 重写了一遍组合优化流程。这篇文章不是教科书复述而是把我从 Markowitz 走到遗传算法这一路上的理论推导、可执行代码、参数经验和踩过的坑一次性讲清楚。如果你正在写自己的 python 量化交易策略代码或者刚接触遗传算法原理想找个落地场景这篇就是给你准备的。2. 先立住理论基础Markowitz 均值-方差模型的核心公式与失效边界2.1 均值-方差模型的数学骨架期望收益、协方差矩阵与有效前沿Markowitz 模型的出发点很简单把组合的收益和风险分别用一个数字来刻画。组合的期望收益是各资产期望收益的加权和公式是E(Rp) Σ wi * E(Ri)其中 wi 是资产 i 的权重Σ wi 1E(Ri) 是资产 i 的历史平均收益。组合的风险用方差或标准差表示公式是σp² ΣΣ wi * wj * σij这里的 σij 是资产 i 和 j 的收益协方差。当 ij 时σii 就是资产 i 的方差。这个式子说明组合的风险不是权重平方的简单累加资产之间的相关性直接影响整体波动。只要相关系数小于 1分散持有就能降低风险这就是不要把鸡蛋放在一个篮子里的数学表达。有了这两个公式优化目标就变成了在给定期望收益 E(Rp) 的水平下最小化 σp²或者在给定风险容忍度 σp 的前提下最大化 E(Rp)。把所有最优解画在一张图上横轴是风险标准差纵轴是收益就得到一条平滑的曲线——有效前沿。落在有效前沿上的组合意味着在相同风险下收益最高或者在相同收益下风险最低。实际用 Python 计算有效前沿时我一般用 scipy.optimize.minimize 做一个 SLSQP 优化。目标函数是组合方差约束条件有两个权重之和等于 1权重大于等于 0不允许做空。代码骨架如下import numpy as np from scipy.optimize import minimize def portfolio_vol(w, cov): # 组合方差w cov w再开方得标准差 return np.sqrt(w cov w) def minimize_vol_for_target_return(target_ret, expected_ret, cov): n len(expected_ret) # 初始权重等权 w0 np.ones(n) / n constraints [ {type: eq, fun: lambda w: w.sum() - 1}, # 权重和1 {type: eq, fun: lambda w: w expected_ret - target_ret} # 组合收益目标 ] bounds [(0, 1) for _ in range(n)] # 不允许做空 res minimize(portfolio_vol, w0, args(cov,), methodSLSQP, boundsbounds, constraintsconstraints) return res.x这段代码的输入 expected_ret 是各资产年化收益向量cov 是协方差矩阵。注意 scipy 的 minimize 默认做的是最小化所以直接传 portfolio_vol 就是最小化标准差。SLSQP 能处理等式和不等式约束但对初始值敏感权重数量超过 20 个时经常收敛到局部最优。我在实盘中还遇到过一种情况两只股票相关性接近 0.95优化器会在它们之间随机分配权重导致解不稳定。2.2 三个让 Markowitz 翻车的场景为什么需要遗传算法Markowitz 模型在教科书里很完美但在实际工程里至少有三个场景会让它直接翻车。第一个场景是整数手数约束。A 股最小买入单位是 100 股1 手港股美股也有类似规则。如果你用 Markowitz 算出某只股票占组合 3.7%但股价是 50 元资金 10 万那么 3.7% 对应 3700 元只能买 700 股3.5 万元占比 35%或 600 股占比 30%离散化后组合比例严重偏离最优解。scipy 的连续优化器根本没法表达必须是 100 股整数倍这个约束。第二个场景是基数约束和交易成本。机构投资者常要求组合最多持有 15 只股票或者换手率不超过 50%。这类约束会让优化问题的可行域变成非凸的离散空间SLSQP 和内点法很容易陷入局部极小甚至找不到可行解。再加上印花税、滑点和佣金目标函数不再是光滑的二次函数而是带着阶梯状跳变的非光滑函数梯度信息几乎失效。第三个场景是收益分布不满足正态假设。2015 年 A 股和 2020 年原油期货都出现过极端尾部行情用历史协方差矩阵算出来的有效前沿在极端行情下组合实际波动比预估高出一大截。Markowitz 本质上是二阶矩优化对异常值极度敏感一个 5 倍标准差的收益样本就能让权重发生剧烈变化。遗传算法GA是另一条路。它不需要目标函数可导也不需要约束连续只要能把解编码成染色体然后定义适应度函数就能在离散空间里搜索近似最优解。遗传算法的原理我在 3.2 节结合代码细讲这里先给结论它不是要替代 Markowitz而是把 Markowitz 的优化目标收益最大化、风险最小化变成适应度函数的一部分再额外叠加整数手数、基数限制等实际约束最后用进化迭代找到工程上能落地的组合。3. 用 Python 从零实现遗传算法组合优化代码、参数与可复现结果3.1 数据准备构造价格序列并计算收益率与协方差为了让你直接复制就能跑我不依赖任何外部数据接口用 numpy 随机生成 6 只资产的日价格数据。真实项目中你可以把这些随机数据替换成你从通达信、Wind 或聚宽导出的 CSV 文件。数据长度我设置为 500 个交易日大约两年这个长度足够计算稳定的协方差矩阵。import numpy as np import pandas as pd np.random.seed(42) # 固定随机种子保证结果可复现 n_assets 6 n_days 500 # 每个资产给定一个基准日收益率年化约 10% ~ 25%和波动率 daily_ret_annual np.array([0.15, 0.10, 0.20, 0.08, 0.12, 0.18]) annual_vol np.array([0.20, 0.15, 0.30, 0.10, 0.18, 0.25]) # 生成相关矩阵让部分资产之间有一定正相关性 corr np.array([ [1.0, 0.6, 0.3, 0.2, 0.4, 0.5], [0.6, 1.0, 0.4, 0.1, 0.3, 0.4], [0.3, 0.4, 1.0, 0.2, 0.5, 0.3], [0.2, 0.1, 0.2, 1.0, 0.1, 0.2], [0.4, 0.3, 0.5, 0.1, 1.0, 0.4], [0.5, 0.4, 0.3, 0.2, 0.4, 1.0] ]) # Cholesky 分解生成带相关性的收益率 L np.linalg.cholesky(corr) daily_returns np.zeros((n_days, n_assets)) for t in range(n_days): z np.random.normal(0, 1, n_assets) # 每个资产的日收益 日基准收益 日波动 * 相关白噪声 daily_returns[t] daily_ret_annual / 252 daily_vol / np.sqrt(252) * (L z) # 将收益率累乘折算成价格序列起点为 100 元 price 100 * np.cumprod(1 daily_returns, axis0) # 计算年化收益率和协方差矩阵 expected_ret np.mean(daily_returns, axis0) * 252 cov_matrix np.cov(daily_returns.T) * 252 # 方便查看数据形态 df_price pd.DataFrame(price, columns[fasset_{i} for i in range(n_assets)]) print(df_price.head()) print(期望收益:, np.round(expected_ret, 6)) print(协方差矩阵形状:, cov_matrix.shape)这段代码里daily_ret_annual / 252是把年化收益转成日收益annual_vol / np.sqrt(252)是把年波动转成日波动。Cholesky 分解是将独立正态分布的白噪声 z 变换成带相关性的噪声这样资产之间的联动关系能体现在协方差矩阵里。你在真实数据上不需要做这一步直接用df_price.pct_change().dropna()算日收益率就行。3.2 遗传算法染色体编码实数权重 整数手数的混合编码遗传算法第一步是定义染色体Chromosome也就是一个可行解的编码方式。常见做法有两种二进制编码和实数编码。投资组合优化里权重是连续的如果用二进制编码需要把每个权重映射到 0 到 1 之间的小数映射精度取决于二进制位数例如 16 位二进制能表达 65536 个离散档位但染色体长度会变成 16 * 资产数导致收敛变慢。我习惯用实数编码每条染色体就是一个长度为 n资产数量的浮点数组每个元素表示该资产的权重。但要注意遗传算法在交叉和变异时生成的子代可能不满足权重之和等于 1的约束所以需要在染色体解码前做归一化。更进一步如果你要做整数手数限制我会把染色体设计成两层一层是浮点权重决定相对比例另一层是整数股数决定实际持仓。这里我们用一种更工程化的编码方式直接把每只股票的配置比例编码成实数不限定深度在适应度计算时把比例映射成组合权重。下面这段代码定义了染色体类的基本结构和一个简单的生成函数class PortfolioChromosome: def __init__(self, weights): # weights 是长度 n_assets 的浮点数组元素可正可负后续会归一化 self.weights np.array(weights, dtypefloat) self.fitness None def normalize(self): # 权重指数归一化保证都是正数且加和1 exp_w np.exp(self.weights) self.weights exp_w / np.sum(exp_w) return self.weights def random_chromosome(n_assets): # 用正态分布随机初始化避免全 0 w np.random.normal(sizen_assets) return PortfolioChromosome(w)normalize使用的 softmax 变换有个好处无论原始weights里是负数还是大数值归一化后都落在 0 到 1 之间且加和为 1直接满足了 Markowitz 的权重约束。同时归一化保持了个体之间的相对差异变异后的染色体不会因为某个权重过大而直接压垮其他权重。如果你不想用 softmax也可以用np.clip(w, 0, None) 除以总和但那种方式容易让负权重变成 0损失一部分探索性。为什么不用简单的w / sum(w)因为遗传算法在随机搜索过程中cross 和 mutate 可能生成负数权重w / sum(w)会让负权和正权抵消加和虽然为 1但某个资产可能出现负权重做空。对大多数个人投资者来说A 股做空机制有限我默认不做空所以用 softmax 强制所有权重为正。如果未来你想加入做空只要改成允许负值并在后面的约束处理里单独判断。3.3 选择、交叉、变异与适应度函数完整可运行代码遗传算法的灵魂在适应度函数。投资组合优化是双目标——收益最大、风险最小我一般把适应度定义为风险调整后收益最常用的是夏普比率Sharpe Ratiofitness expected_return / portfolio_volatility夏普比率的分母是组合标准差分子是超额收益或者直接是收益这里简化。适应度值越大个体越优秀。如果要惩罚高换手可以在分子或分母上减去交易成本如果要限制持仓数量可以在适应度上乘以一个惩罚系数。我把这些选项都写进下面的完整代码里。import numpy as np def fitness_function(chromosome, expected_ret, cov_matrix, penalty_cost0.0): 计算个体的适应度夏普比率 - 交易成本惩罚 chromosome: PortfolioChromosome 对象 expected_ret: 各资产年化收益向量 cov_matrix: 协方差矩阵 penalty_cost: 持仓数量惩罚系数0 表示鼓励集中持仓 w chromosome.normalize() # 得到合规权重 port_ret np.dot(w, expected_ret) port_vol np.sqrt(np.dot(w.T, np.dot(cov_matrix, w))) # 避免除零 if port_vol 1e-8: return -1e9 # 非零权重数量越少penalty 越小能增加适应度因为减去的是正数 num_holdings np.sum(w 0.01) penalty penalty_cost * num_holdings return port_ret / port_vol - penalty选择阶段我采用锦标赛选择Tournament Selection每次随机抽出 3 个个体保留适应度最高的 1 个进入下一代。这样的好处是选择压力和种群多样性可以同时控制不至于让某个超级个体迅速占据整个种群。交叉阶段对实数编码我使用模拟二进制交叉SBX也可以直接用均匀交叉随机选一个交叉点交换两条染色体交叉点之后的部分。变异阶段对实数编码采用高斯扰动给每个基因加上一个均值为 0标准差为 sigma 的随机数。下面是一个完整的遗传算法主循环参数都写在顶部def genetic_optimizer(expected_ret, cov_matrix, pop_size100, generations200, tournament_size3, cross_prob0.8, mut_prob0.2, mut_scale0.05, penalty_cost0.0, elapsed_penalty0.0): n_assets len(expected_ret) # 初始化种群 population [PortfolioChromosome(np.random.normal(sizen_assets)) for _ in range(pop_size)] best_fitness_history [] for gen in range(generations): # 计算适应度 for ind in population: ind.fitness fitness_function(ind, expected_ret, cov_matrix, penalty_cost) # 记录最好个体 population.sort(keylambda x: x.fitness, reverseTrue) best_fitness_history.append(population[0].fitness) new_population [] # 精英保留直接保留前两个个体 new_population.extend(population[:2]) while len(new_population) pop_size: # 锦标赛选择 def tournament_select(): candidates np.random.choice(population, tournament_size, replaceFalse) return max(candidates, keylambda x: x.fitness) parent1 tournament_select() parent2 tournament_select() # 交叉 if np.random.rand() cross_prob: child_w1 (parent1.weights parent2.weights) / 2 child_w2 (parent1.weights - parent2.weights) / 2 # 两种交叉结果都保留增加多样性 child1 PortfolioChromosome(child_w1) child2 PortfolioChromosome(child_w2) else: child1 PortfolioChromosome(parent1.weights.copy()) child2 PortfolioChromosome(parent2.weights.copy()) # 变异 for child in (child1, child2): if np.random.rand() mut_prob: # 随机选一个维度做高斯扰动 pos np.random.randint(0, n_assets) child.weights[pos] np.random.normal(0, mut_scale) new_population.extend([child1, child2]) population new_population[:pop_size] # 最终选择最优个体 for ind in population: ind.fitness fitness_function(ind, expected_ret, cov_matrix, penalty_cost) best max(population, keylambda x: x.fitness) best_weights best.normalize() return best_weights, best_fitness_history代码里有两个细节值得解释。第一交叉部分我用的是算术交叉子代权重是父母权重的加权平均和差值这比单点交叉更适合实数编码因为它能保持父代的数值特征比例。第二变异只随机选中一个维度加噪声而不是全部维度。这是因为投资组合约束强如果同时变异多个维度很容易破坏权重之间的平衡导致归一化后大部分权重趋近于 0算法退化成随机搜索。这种单点高斯变异策略在处理高维约束优化时比全维变异高效得多。3.4 关键参数设置种群大小、迭代次数、变异率的经验区间遗传算法最让人头疼的就是参数玄学一堆旋钮不知道怎么拧。我根据自己的实验和几个开源项目比如 deap 文档的经验整理了一个从能用走向好用的参数区间见下表。参数经验区间作用设置建议种群大小 pop_size60 ~ 200种群多样性资产数量小于 20 时用 100资产数量大于 50 时建议 200迭代次数 generations150 ~ 500搜索代数先跑 200 代看收敛曲线如果适应度还在上涨就加代交叉概率 cross_prob0.7 ~ 0.9探索新区域0.8 是默认值问题越复杂越接近 0.9变异概率 mut_prob0.1 ~ 0.3防止早熟0.2 左右变异率太高会让算法退化成随机搜索变异尺度 mut_scale0.01 ~ 0.1单步扰动幅度权重是实数0.05 对 6 资产问题表现较好锦标赛大小 tournament_size2 ~ 5选择压力越大收敛越快但容易早熟3 是均衡值惩罚系数 penalty_cost0 ~ 0.1约束软化若限制持仓数量从 0.01 开始调这些参数相互牵连比如增大种群可以减缓变异率过高带来的随机性但会拖慢迭代速度。我的调参方法很简单先用默认参数跑一遍观察best_fitness_history最后 50 代是否平稳。如果不平稳说明迭代不够如果平稳但适应度很低说明种群多样性不够可以调大变异率或种群大小。切忌一次调多个参数你会在参数迷宫里出不来的。一个值得注意的边界是当资产数量超过 30 时普通遗传算法的收敛速度会急剧下降。因为搜索空间维度变高而遗传算法的交叉操作在 30 维实数向量上很难生成优于父代的子代。这种情况下我一般会改用 NSGA-II多目标遗传算法把收益和风险分别作为两个目标去优化而不是合并成夏普比率。但那是另一个话题本文先聚焦在 6~10 个资产规模的组合上这个规模下 GA 足够高效。4. 避坑指南遗传算法优化组合时最常见的 5 个坑4.1 坑一权重归一化后整个种群全部坍塌成同一种解现象跑完 200 代后best_fitness_history前 100 代还正常后面突然变成一条直线适应度不再变化种群中所有染色体归一化后都差不多。原因你用的归一化方式可能是w / sum(w)而变异或交叉产生的权重包含正负号。当某个个体某个维度权重异常大时归一化后其他维度几乎为 0这个个体拥有极高的适应度锦标赛选择很快就把它复制满整个种群——早熟收敛。再加上实数编码的算术交叉对极端值没有抑制作用搜索彻底停滞。解决改用 3.2 节的 softmax 归一化。softmax 是一个平滑函数即使某个基因值为 10归一化后权重也就是 0.999不会直接抹掉其它基因。同时把变异限制在单维度并且将变异尺度mut_scale控制在 0.05 左右避免出现极大离群值。我还习惯在每一代强制随机注入 2% 的完全随机个体类似生态学里的移民防止早熟。4.2 坑二协方差矩阵不可逆适应度计算直接 Nan现象代码跑到一半报错或者适应度变成nan最终结果全为 0。原因当资产数量大于样本天数的 1/3或者资产间高度相关时np.cov(daily_returns.T)得到的是一个近乎奇异的矩阵。比如持仓里同时有 贵州茅台 和 五粮液协方差矩阵的行列式趋近于 0求逆时溢出为 inf计算组合波动平方根时出现 NaN。解决给协方差矩阵加一个很小的正则化项也就是把cov_matrix lambda * np.eye(n_assets)lambda 取 1e-6 或 1e-8。这个操作也叫 shrinkage收缩相当于把协方差矩阵朝单位矩阵的方向拉提高数值稳定性。更专业的做法是使用 Ledoit-Wolf 收缩sklearn 里有现成实现sklearn.covariance.LedoitWolf我这边为了避免额外依赖直接加了一个常数。代码调整如下cov_matrix_reg cov_matrix 1e-6 * np.eye(n_assets)4.3 坑三把适应度设置成收益越大越好结果组合风险爆表现象遗传算法找出一个权重年化收益 45%回测曲线像坐过山车最大回撤 60%根本不敢实盘。原因你只把组合收益当适应度没有除以风险项。遗传算法是个无情的优化器只要没有惩罚它就会把权重集中到历史收益最高的单个资产上因为单资产夏普比率可能不是最优但单纯收益率最高是确定的。这在样本内是真实结果在样本外必然翻车。解决适应度里至少包含风险项。最简做法是直接用夏普比率收益率 / 波动率再加一条惩罚如果组合波动超过你设定的最大可承受波动如 25%在适应度上减去一个巨额惩罚。例如def fitness_with_vol_cap(chromosome, expected_ret, cov_matrix, max_vol0.25): w chromosome.normalize() port_ret np.dot(w, expected_ret) port_vol np.sqrt(np.dot(w.T, np.dot(cov_matrix, w))) if port_vol max_vol: return -1e6 * (port_vol - max_vol) # 超限惩罚 return port_ret / port_vol4.4 坑四把随机种子固定一次就默认算法性能稳定现象上一次运行得到的最优组合权重是 [0.1, 0.2, 0.3, 0.4, 0.0, 0.0]重跑一次变成 [0.0, 0.4, 0.1, 0.2, 0.3, 0.0]差异巨大你觉得算法不稳定。原因遗传算法是随机搜索算法单次运行的结果具有随机性。如果你固定了np.random.seed那结果可复现但你只看到了一条可能的路径。组合优化问题往往存在多个局部最优解这些解在适应度上很接近但权重结构完全不同。单次运行恰好落在一个峰上并不能代表算法性能。解决正确做法是多次运行至少 10 次建议 20 次用不同的随机种子然后把每次的最优权重存下来。最终决策时看这些权重的均值或者中位数而不是某一次的具体值。我一开始偷懒只跑一次结果上实盘前换了个种子最优解从满仓消费股变成了满仓成长股血泪教训。4.5 坑五用未来的数据参与协方差矩阵计算回测数据泄漏现象遗传算法在样本内适应度很高样本外实盘完全失真甚至亏损。原因你把整段历史数据同时用于计算期望收益、协方差矩阵以及计算适应度的回测区间。这意味着遗传算法在搜索时已经看到了未来。比如你用 2022 年到 2024 年的数据优化然后拿同一段数据做回测验证这属于数据泄漏。真实交易里你只能用当前时点之前的数据做优化再拿到未来或者样本外数据上验证。解决做一个时间切分。比如数据有 1000 天前 800 天作为训练集计算期望收益和协方差矩阵跑遗传算法后 200 天作为测试集用训练集找到的最优权重直接做收益和回撤模拟。如果你需要做滚动优化就用 walk-forward 方法每次用过去 250 天训练未来 20 天测试逐渐滑动窗口。这是量化策略开发的标准姿势不能省略。5. 进阶验证用回测与稳定性检验确认遗传算法找到的组合以及可复现的代码骨架当遗传算法返回一组权重后第一个动作不是下单而是验证这组权重是不是过拟合的偶然产物。我做三件事样本外回测、权重稳定性测试、参数扰动测试。样本外回测最简单把 3.1 节的数据切成两半前 60% 训练后 40% 测试。训练阶段得到最优权重然后在测试阶段每天用这个固定权重计算组合净值统计年化收益、波动、最大回撤和夏普比率。如果测试夏普比率为正且明显高于单资产说明组合优化有效。如果测试结果很差哪怕训练集很好也要回头检查是不是适应度函数设定有问题。下面是一个极简的样本外测试代码def backtest(weights, daily_returns_test): # 每日组合收益 当日各资产收益 * 权重 port_daily daily_returns_test weights cumulative np.cumprod(1 port_daily) total_return cumulative[-1] / cumulative[0] - 1 annualized_vol np.std(port_daily, ddof1) * np.sqrt(252) # 简单夏普无风险利率假设为0 sharpe np.mean(port_daily) / np.std(port_daily, ddof1) * np.sqrt(252) # 最大回撤 peak np.maximum.accumulate(cumulative) max_drawdown np.max(peak - cumulative) / np.max(peak) return {收益: total_return, 年化波动: annualized_vol, 夏普: sharpe, 最大回撤: max_drawdown}权重稳定性测试则是把数据切成多个训练窗口比如 2020-2021、2021-2022、2022-2023在每个窗口上分别跑遗传算法得到三组权重。如果这三组权重的结构差异极大比如第一次全仓食品饮料第二次全仓半导体说明优化结果对样本区间太敏感实盘中换一个市场风格就失效了。我一般用两个指标来衡量权重向量间的平均绝对差Mean Absolute Difference以及重叠度非零权重资产的重叠比例。平均绝对差小于 0.15重叠度超过 0.6我才认为组合是稳定的。参数扰动测试针对遗传算法自身的参数把种群大小、变异率等每个参数单独改变 ±20%重新优化看最终夏普比率是否变化不大。如果夏普从 1.2 掉到 0.6说明你当前的解是参数悬崖上的尖峰实盘稍微偏离模型假设就会崩溃。我会对每个参数跑 5 次取最大和最小夏普如果差值超过 0.5就需要简化模型或者增加约束。做完这三步验证后遗传算法给出的权重才具备落地价值。我现在的习惯是把 3.2 到 3.3 的代码封装成一个optimize_portfolio()函数输入历史收益率矩阵和约束参数输出收敛曲线和最优权重。更进一步的扩展方向有两个一是基于遗传算法实现风险平价Risk Parity的变体把目标换成各个资产对组合风险的贡献度相等二是把交易成本直接建模在每个染色体的适应度里用分段函数表示不同换手率下的成本这样优化出来的组合更贴近真实交易。我个人最受用的一条经验是永远不要相信单次运行的最优权重宁可多跑十几分钟做多次采样也不要怀着侥幸心理直接实盘。投资组合优化看起来是一堆数学公式其实最终拼的是对不确定性的敬畏。这套遗传算法代码我已经用在好几个个人资产配置小工具里每次跑完都会顺手画一张收敛曲线看到适应度最后 50 代平平稳稳心里才踏实。希望这篇从 Markowitz 到遗传算法的拆解能帮到你让你在组合优化的路上少踩几个我踩过的坑。本文还有配套的精品资源点击获取