3. 遗传算法入门:基因编码、选择、交叉、变异,一个简单的交易策略优化案例
好,咱们今天聊点硬核的——遗传算法。
说实话,我第一次接触这东西是在做CTA策略参数优化的时候。那时候我还在用网格搜索,跑一次要三天。后来一个老前辈跟我说:“你试试遗传算法,别傻乎乎地全跑一遍。” 嗯,试完之后,真香。
遗传算法(GA)说白了,就是模拟生物进化过程来找最优解。你想想看,大自然几亿年进化出人类这么复杂的生物,靠的就是“优胜劣汰、适者生存”那套逻辑。我们做量化交易,不也是在找那个最能赚钱的策略参数吗?
核心思想: 把策略参数看成“基因”,把策略表现看成“适应度”。好的基因留下来,差的淘汰掉,再通过交叉、变异产生新个体。一代代迭代下去,最终收敛到最优解。
3.1 基因编码:怎么把策略参数变成“染色体”
先解决第一个问题:怎么把策略参数表示成计算机能理解的形式?
我个人习惯用二进制编码。举个例子,假设我们有个简单的双均线策略,需要优化两个参数:
- 短期均线周期:范围 5~50
- 长期均线周期:范围 20~200
每个参数用10位二进制表示,两个参数拼起来就是一条20位的染色体。比如:
短期均线(10位) | 长期均线(10位)
0000011111 | 0010101010
→ 短期 = 31, 长期 = 42
小技巧: 我在项目中遇到过参数范围跨度很大的情况,比如一个是1~10,另一个是100~1000。这时候建议用不同位数的二进制编码,或者直接用实数编码。别死磕二进制。
当然,也有用实数编码的,直接把参数值当基因。我个人觉得,二进制编码在交叉和变异时更自然,但实数编码更直观。看个人喜好。
3.2 选择:谁活下来,谁被淘汰
有了初始种群(比如随机生成100组参数),接下来就要算适应度了。
适应度函数就是你的目标函数。在交易策略里,可以是夏普比率、年化收益率、最大回撤的倒数…… 我一般用夏普比率 × 年化收益的组合,既考虑收益又考虑风险。
算完适应度,就要选择了。常用的方法有:
- 轮盘赌选择: 适应度越高,被选中的概率越大。就像转盘,大块头的区域更容易被指针点到。
- 锦标赛选择: 随机挑几个个体,选里面最好的。我比较喜欢这个,简单粗暴,不容易早熟收敛。
- 精英保留: 把最好的几个直接复制到下一代,保证最优解不会丢失。
注意: 我曾经犯过一个错误——只用轮盘赌,结果适应度最高的个体占了绝对优势,种群迅速失去多样性,陷入局部最优。后来我加了精英保留和锦标赛选择,效果好了很多。
3.3 交叉:让好基因组合起来
选择完父代,就要让它们“生孩子”了。交叉操作就是模拟生物的有性繁殖。
最常用的是单点交叉:随机选一个交叉点,交换两个父代染色体后半段。比如:
父代A: 0000011111 | 0010101010
父代B: 1111100000 | 0101010101
交叉点在第10位之后
子代1: 0000011111 | 0101010101
子代2: 1111100000 | 0010101010
还有多点交叉、均匀交叉等变种。我个人经验是,单点交叉在大多数情况下够用了。交叉概率一般设在0.6~0.9之间,太高容易破坏好基因,太低进化太慢。
3.4 变异:引入随机性,防止陷入局部最优
交叉之后,还要加点“随机扰动”——这就是变异。
二进制编码的变异很简单:随机选几个位,0变1,1变0。变异概率通常设得很低,0.01~0.1之间。太高了就成了随机搜索,太低了又容易早熟。
为什么需要变异? 你想想看,如果所有个体都差不多,交叉也产生不了新东西,那算法就卡住了。变异就像给种群打了一针“兴奋剂”,让它跳出局部最优。
我记得有一次优化一个多因子选股模型,种群迭代到50代时适应度就不动了。我检查了一下,发现所有个体的参数都集中在某个小区域。后来我把变异概率从0.05提高到0.15,又跑了20代,找到了一个更好的解。
3.5 完整流程:一个简单的交易策略优化案例
好,理论说完了,咱们来点实战。假设我们要优化一个简单的双均线金叉死叉策略:
- 短期均线上穿长期均线 → 买入
- 短期均线下穿长期均线 → 卖出
需要优化的参数:短期周期(5~50)、长期周期(20~200)。适应度函数用夏普比率。
完整流程如下:
- 初始化: 随机生成50组参数,每组用20位二进制编码
- 评估: 对每组参数回测,计算夏普比率
- 选择: 用锦标赛选择(每次随机挑3个,选最好的)选出25对父代
- 交叉: 每对父代以0.8的概率进行单点交叉,生成50个子代
- 变异: 每个子代的每个位以0.05的概率翻转
- 精英保留: 把上一代最好的2个个体直接复制到下一代
- 重复: 回到步骤2,直到达到最大代数(比如100代)或适应度不再提升
下面我用Python伪代码展示核心逻辑:
# 伪代码,实际实现需要更多细节
def genetic_algorithm():
# 初始化种群
population = [random_chromosome() for _ in range(50)]
for generation in range(100):
# 评估适应度
fitness = [evaluate(chrom) for chrom in population]
# 精英保留
elites = select_elites(population, fitness, 2)
# 选择父代
parents = tournament_selection(population, fitness, 25)
# 交叉生成子代
offspring = []
for i in range(0, len(parents), 2):
if random() < 0.8:
child1, child2 = crossover(parents[i], parents[i+1])
else:
child1, child2 = parents[i], parents[i+1]
offspring.extend([child1, child2])
# 变异
for child in offspring:
mutate(child, rate=0.05)
# 新种群 = 精英 + 子代
population = elites + offspring
return best_chromosome(population, fitness)
避坑指南: 我曾经把精英保留数量设得太大(比如10个),结果种群多样性急剧下降。建议精英数量不超过种群总数的5%。另外,记得在每次迭代时记录最佳适应度,方便调试。
3.6 遗传算法的优缺点
| 优点 | 缺点 |
|---|---|
| 不需要梯度信息,适用于非连续、非线性问题 | 计算量大,每次迭代都要评估整个种群 |
| 天然支持并行计算 | 容易早熟收敛,陷入局部最优 |
| 可以处理大量参数 | 参数(种群大小、交叉率、变异率)调起来麻烦 |
| 不容易陷入局部最优(相比梯度下降) | 没有理论保证收敛到全局最优 |
嗯,说白了,遗传算法不是万能的。但它确实是个好工具,尤其在参数空间大、目标函数复杂的时候。我现在的习惯是:先用遗传算法跑一遍,找到大概的最优区域,再用局部搜索精调。两相结合,效果最好。
最后,送你一张遗传算法的流程图,方便你理解整个进化过程:
这张图把整个流程串起来了。你照着这个逻辑写代码,基本不会跑偏。
公众号:蓝海资料掘金营,微信deep3321