第十一章:粒子群优化调优
粒子群优化,英文叫 Particle Swarm Optimization,简称 PSO。这名字听着挺唬人,其实说白了,就是模拟一群鸟找食物的过程。每只鸟都不知道食物在哪,但它们会互相交流,共享信息,最终整个鸟群都能找到食物。
我最早接触 PSO 是在做股指期货的跨期套利时。当时网格搜索跑了一整夜,结果还不理想。后来换成 PSO,十分钟就找到了不错的参数组合。嗯,从那以后,我就把这方法当成了调参的标配。
粒子群算法的核心思想
想象一下,你有一群粒子,每个粒子代表一组参数。比如在基差交易中,一个粒子可能包含「开仓阈值=0.8,止损比例=2%,持仓周期=5天」这三个参数。
这些粒子在参数空间里飞来飞去。每个粒子都有自己的位置和速度。位置就是当前参数值,速度决定了下一步往哪飞。
关键来了——每个粒子会记住两件事:
- 个体最优:自己飞过的最好位置
- 全局最优:整个群体中最好的位置
粒子每次移动,都会参考这两个信息。说白了,就是既相信自己过去的经验,也借鉴同伴的成功经验。
核心公式(简化版):
新速度 = 惯性 × 当前速度 + 个体学习 × (个体最优 - 当前位置) + 群体学习 × (全局最优 - 当前位置)
新位置 = 当前位置 + 新速度
粒子速度与位置更新
速度更新是 PSO 的灵魂。我习惯把速度理解成「调整的幅度和方向」。正值表示往参数增大的方向飞,负值表示往减小的方向飞。
举个例子,假设当前开仓阈值是 0.8,个体最优是 0.85,全局最优是 0.9。那么速度更新公式中的「个体学习」部分会推动粒子往 0.85 方向移动,「群体学习」部分会推动往 0.9 方向移动。两者合力,粒子就会朝着 0.85~0.9 之间的某个位置飞去。
位置更新就简单了,直接在当前位置上加上速度向量。嗯,这里要注意一点:参数是有边界的。比如止损比例不能是负数,持仓周期不能超过 30 天。我一般会在位置更新后加一个边界检查,把超出范围的参数拉回来。
我的经验:速度的初始值不要设太大。我刚开始做时,把速度范围设成了 [-10, 10],结果粒子到处乱飞,根本收敛不了。后来改成 [-1, 1],效果就好多了。具体范围取决于你的参数取值范围,一般取参数范围的 10%~20% 比较合适。
惯性权重
惯性权重,英文叫 inertia weight,是控制粒子「探索」和「开发」平衡的关键参数。
- 惯性权重大:粒子倾向于保持原有方向,探索新区域的能力强
- 惯性权重小:粒子容易被拉向最优位置,局部搜索能力强
我个人的习惯是让惯性权重随时间递减。一开始设大一点,比如 0.9,让粒子充分探索参数空间。随着迭代次数增加,逐渐降到 0.4,让粒子精细搜索最优区域。
为什么会这样?你想想看,刚开始时我们不知道最优参数在哪,需要广撒网。到了后期,已经找到了不错的区域,就该收网了。
避坑指南:我曾经试过固定惯性权重为 0.5,结果算法陷入了局部最优。后来改成线性递减,效果明显改善。但递减速度也要控制好,降得太快,粒子还没探索完就收网了;降得太慢,后期又不够精细。
局部最优与全局最优
这两个概念是 PSO 的核心。全局最优是整个群体找到的最好位置,局部最优是每个粒子自己找到的最好位置。
但这里有个陷阱——「全局最优」其实只是当前找到的最好位置,不一定是真正的全局最优。算法可能陷入局部最优,就是所有粒子都聚集在一个不是最好的区域里。
怎么避免?我常用的方法有:
- 增加粒子数量:粒子越多,覆盖的参数空间越大
- 引入变异机制:随机让一些粒子跳出现有位置
- 多起点初始化:让粒子初始位置均匀分布在参数空间
我记得有一次做螺纹钢的基差策略调优,粒子群全部收敛到了一个局部最优,年化收益只有 8%。后来我增加了粒子数量,从 20 个增加到 50 个,结果找到了一个年化 15% 的参数组合。嗯,多花点计算时间,值!
粒子群调优实践
好了,理论说完了,咱们直接上代码。下面是一个完整的 PSO 调优示例,用于优化基差交易策略的三个参数。
import numpy as np
# 策略评估函数(模拟)
def evaluate_strategy(params):
threshold, stop_loss, hold_days = params
# 这里应该是你的回测逻辑
# 返回夏普比率作为适应度
sharpe = - (threshold - 0.8)**2 - (stop_loss - 0.02)**2 - (hold_days - 5)**2
return sharpe + np.random.normal(0, 0.1) # 加一点噪声模拟市场随机性
# PSO 参数
n_particles = 30
n_iterations = 50
w_start = 0.9 # 初始惯性权重
w_end = 0.4 # 最终惯性权重
c1 = 2.0 # 个体学习因子
c2 = 2.0 # 群体学习因子
# 参数边界
bounds = np.array([
[0.5, 1.0], # 开仓阈值
[0.01, 0.05], # 止损比例
[1, 10] # 持仓周期
])
# 初始化粒子
positions = np.random.uniform(bounds[:, 0], bounds[:, 1], (n_particles, 3))
velocities = np.random.uniform(-0.1, 0.1, (n_particles, 3))
personal_best = positions.copy()
personal_best_scores = np.array([evaluate_strategy(p) for p in positions])
global_best = personal_best[np.argmax(personal_best_scores)]
global_best_score = np.max(personal_best_scores)
# 主循环
for iteration in range(n_iterations):
w = w_start - (w_start - w_end) * iteration / n_iterations
for i in range(n_particles):
# 更新速度
r1, r2 = np.random.random(3), np.random.random(3)
velocities[i] = (w * velocities[i] +
c1 * r1 * (personal_best[i] - positions[i]) +
c2 * r2 * (global_best - positions[i]))
# 更新位置
positions[i] = positions[i] + velocities[i]
# 边界处理
positions[i] = np.clip(positions[i], bounds[:, 0], bounds[:, 1])
# 评估
score = evaluate_strategy(positions[i])
# 更新个体最优
if score > personal_best_scores[i]:
personal_best[i] = positions[i].copy()
personal_best_scores[i] = score
# 更新全局最优
if score > global_best_score:
global_best = positions[i].copy()
global_best_score = score
if iteration % 10 == 0:
print(f"迭代 {iteration}: 全局最优得分 = {global_best_score:.4f}")
print(f"\n最优参数: 阈值={global_best[0]:.3f}, 止损={global_best[1]:.3f}, 持仓={int(global_best[2])}天")
print(f"最优得分: {global_best_score:.4f}")
实用建议:实际应用中,evaluate_strategy 函数里放你的完整回测逻辑。我一般会加上交易成本、滑点等细节,否则找到的参数在实盘中会失效。另外,建议把回测结果缓存起来,同一个参数组合不要重复计算。
PSO 调优的完整流程
下面这张图展示了 PSO 调优的完整流程,从初始化到最终输出最优参数:
这个流程看起来简单,但实际应用中要注意几个细节:
- 适应度函数设计:不要只用收益率,我一般用夏普比率或卡玛比率,这样能找到风险调整后的最优参数
- 早停机制:如果连续 10 次迭代全局最优都没变化,可以提前终止,节省计算时间
- 多次运行:PSO 有随机性,我习惯跑 5~10 次,取最优结果
重要提醒:PSO 找到的参数,一定要做样本外测试。我曾经找到一个在回测中夏普 2.5 的参数组合,结果样本外只有 0.8。后来发现是过拟合了。建议把数据分成训练集和测试集,用训练集调参,用测试集验证。
好了,粒子群优化调优的内容就这些。说白了,PSO 就是让一群「参数粒子」在参数空间里飞,互相学习,最终找到最优解。这个方法比网格搜索高效得多,尤其适合参数多、范围大的场景。
我个人建议,刚开始用 PSO 时,先跑一个简单的测试案例,把惯性权重、学习因子这些参数调明白。等熟悉了,再应用到实际的基差交易策略中。嗯,实践出真知,多试几次就找到感觉了。