射箭算法(Archery Algorithm, AA)·进阶篇
archer算法里的概率轮盘与位置刷新
这段逻辑实现的是基于适应度排名的轮盘赌选择,再用高斯扰动更新种群坐标。前两个循环先算每个个体的相对适应度 P[i]=a[i].f-F_worst,累加后归一化,并构造累积概率数组 C[i],为后续按概率抽「弓箭手」做准备。 核心更新段里,对每个个体的每个维度 c,先由 u.RNDprobab() 生成随机数 r,在 C 数组里线性查找选中索引 k(while 循环保证 k 不超过 popSize-1)。若 u.RNDbool() 小于 inhProbab 则直接继承 a[k] 的坐标,否则进入公式(5)(6)的更新。 更新时 I 取 1 或 2(MathRound(1+RNDprobab())),rnd 来自均值为0、边界-1到1、截断8倍的高斯分布。当选中弓箭手适应度优于当前个体,新坐标 = 旧坐标 + rnd*(a[k].c[c] - I*旧坐标),反之符号互换。最后用 SeInDiSp 把 x 夹到 [rangeMin,rangeMax] 并按 rangeStep 离散化。 Revision 函数做两件事:扫描种群记录全局最佳 fB 与 cB;若某个体当前适应度优于其历史 fPrev,则更新 fPrev 与 cPrev。外汇与贵金属市场波动剧烈、杠杆风险高,这类优化器实盘前务必在 MT5 策略测试器用历史数据验证收敛稳定性。
for (class="type">int i = class="num">0; i < popSize; i++) { P [i] = a [i].f - F_worst; sum += P [i]; } for (class="type">int i = class="num">0; i < popSize; i++) { P [i] /= sum; C [i] = (i == class="num">0) ? P [i] : C [i - class="num">1] + P [i]; } class="type">class="kw">double x; for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { class=class="str">"cmt">// Select archer(k) class="kw">using cumulative probability class="type">int k = class="num">0; class="type">class="kw">double r = u.RNDprobab(); class="kw">while (k < popSize - class="num">1 && C [k] < r) k++; if (u.RNDbool() < inhProbab) { x = a [k].c [c]; } else { class=class="str">"cmt">// Update position class="kw">using Eq. (class="num">5) and(class="num">6) class="type">class="kw">double I = MathRound(class="num">1 + u.RNDprobab()); class="type">class="kw">double rnd = u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">8); if (a [k].f > a [i].f) { x = agent [i].cPrev [c] + rnd * (a [k].c [c] - I * agent [i].cPrev [c]); } else { x = agent [i].cPrev [c] + rnd * (agent [i].cPrev [c] - I * a [k].c [c]); } } a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_AAm::Revision() { class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int ind = -class="num">1; for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f > fB) { fB = a [i].f; ind = i; } } if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">//---------------------------------------------------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f > agent [i].fPrev) { agent [i].fPrev = a [i].f; ArrayCopy(agent [i].cPrev, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } }
◍ 射箭算法加信息交换后的实测分水岭
原始射箭算法里,射手之间没有直接通信,只靠正态分布在坐标层面间接耦合。我补了一段 inhProbab 逻辑,让个体以给定概率互递信息,改完的版本叫 AAm。 原版在 10000 次函数评估下,5/25/500 个 Hilly 地形得分分别是 0.66995、0.37356、0.25754;Forest 是 0.38167、0.19930、0.15338;Megacity 是 0.40769、0.17908、0.10005。总分 2.72222,占满分 9 分的 30.25%。 AAm 把概率参数设为 0.3 后,Hilly 前两组跳到 0.93532 和 0.67983,Forest 首组也从 0.38 提到 0.57351;总分 3.89379,占比 43.26%,相对原版提升超过 13 个百分点。分散性只集中在坐标数少的函数(5 个地形),500 地形下两者几乎持平。 横排 33 个优化器里 AAm 排第 26,前面 ANS、CLA 这些占了 60% 以上满分;但 AAm 在少峰值函数上咬得住第一梯队,多峰值深谷里掉得比进化策略快。MT5 上接 EA 前,建议先拿 5 Hilly 跑一遍确认 inhProbab 的 0.3 是不是你品种的最优概率。
「群智能算法回测指标横向对比」
上面这张表是把 34~45 号优化器跑在同一组历史行情上的原始输出,每一行对应一种算法,后面跟着 12 个统计量加综合得分与排名。FAm 萤火虫算法综合分 3.048、排名 33.87,是这截里最高的;BA 蝙蝠算法掉到 2.423、排名 26.93,AAA 人工藻类算法 2.361 紧随其后。 单看前三列(可理解为三组周期下的误差类指标),FAm 是 0.58634 / 0.47228 / 0.32276,GSA 引力搜索是 0.64757 / 0.49197 / 0.30062,BFO 细菌觅食是 0.61171 / 0.43270 / 0.31318——数值越低通常代表拟合偏移越小,但综合分还叠了后面几列的稳定性权重,不能直接拿单列排序。 PSO 粒子群优化综合分 2.230、排名 24.77,SFL 混合蛙跳 2.104、排名 23.38 是这截垫底。外汇与贵金属品种波动有跳空和滑点,这类回测分只说明样本内可能倾向某算法更稳,实盘仍属高风险,换周期或品种结论可能反转。 代码里这段是个体继承逻辑:以 inhProbab 概率直接从父代第 k 个个体的第 c 维抄值,属于典型的社会学习算子,开 MT5 把 inhProbab 从 0.5 调到 0.8 能直观看到种群收敛速度变化。
if (u.RNDbool() < inhProbab) { x = a [k].c [c]; }
把射箭者的靶子按水平反向缩放
原始版用 if-else 决定智能体怎么挪位置,这段逻辑被整段抽掉,换成按适应度反比调靶子大小的思路。射箭者越菜,箭散得越开;水平越高,就在附近细抠。 新插入的三行里,Scale 函数拿 minFF、maxFF 把智能体 i 和 k 的适应度压到 0~1,得到 prob1、prob2。新坐标由 agent[i].cPrev[c]、a[k].c[c] 和随机因子 rnd 算出来,移动幅度受「1 减去两者概率和」缩放——靶子大小随射箭者质量反比伸缩,分布仍服从正态分布。 跑 10000 次的回测:Hilly 5/25/500 维得分 0.917 / 0.709 / 0.422,Forest 同维 0.925 / 0.758 / 0.353,Megacity 0.674 / 0.552 / 0.237,总分 5.5476(61.64%)。收敛稳,能咬住函数曲面的关键区。 把「1 减概率和」改成「2 减」,乘数变成 0~2,步子迈更大。中等维 Forest 25 维从 0.758 跳到 0.909,但 500 维 Megacity 从 0.237 跌到 0.105,总分 5.6192(62.44%)虽高,高维已崩。原版更实用,留作主修改版,排在 45 强里的第 7。 外汇与贵金属参数优化属高风险,回测分高不保证实盘正期望,开 MT5 用 AAm.zip 自跑 500 维函数可见高维退化。
class="type">void C_AO_AAm::Moving() { class=class="str">"cmt">//---------------------------------------------------------------------------- if (!revision) { for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
◍ 适应度转概率与弓箭手位置更新
群体优化里先把每个个体的适应度映射成被选概率。代码先找最差适应度 F_worst,再用 P[i]=a[i].f-F_worst 做平移,使所有 P 非负,随后除以 sum 归一化,并累加出 C 数组作为轮盘赌区间——这是标准做法,popSize 个个体对应 C[popSize-1] 应逼近 1.0。 选‘弓箭手’时丢一个均匀随机数 r,沿 C 数组线性扫描到首个 C[k]>=r 停下,时间复杂度 O(popSize)。若 u.RNDbool() 小于 inhProbab 则直接继承 a[k].c[c],否则进入位置扰动。 扰动项用高斯噪声 rnd=u.GaussDistribution(0,-1,1,8) 乘上相对位移,并乘权重 (1-prob1-prob2);prob1/prob2 由 u.Scale 把个体适应度从 [minFF,maxFF] 压到 [0,1]。当两者适应度都高时权重趋近负数,位置可能反向跳跃,增加逃离局部极值概率。外汇与贵金属波动剧烈,这类随机搜索参数若直接用于实盘信号,需先在 MT5 策略测试器跑多品种回测验证稳定性。
class="type">class="kw">double P [], C []; ArrayResize(P, popSize); ArrayResize(C, popSize); class="type">class="kw">double F_worst = DBL_MAX; class="type">class="kw">double sum = class="num">0; for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f < F_worst) F_worst = a [i].f; } for (class="type">int i = class="num">0; i < popSize; i++) { P [i] = a [i].f - F_worst; sum += P [i]; } for (class="type">int i = class="num">0; i < popSize; i++) { P [i] /= sum; C [i] = (i == class="num">0) ? P [i] : C [i - class="num">1] + P [i]; } class="type">class="kw">double x; class="type">class="kw">double maxFF = fB; class="type">class="kw">double minFF = DBL_MAX; class="type">class="kw">double prob1; class="type">class="kw">double prob2; for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f < minFF) minFF = a [i].f; } for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { class="type">int k = class="num">0; class="type">class="kw">double r = u.RNDprobab(); class="kw">while (k < popSize - class="num">1 && C [k] < r) k++; if (u.RNDbool() < inhProbab) { x = a [k].c [c]; } else { class="type">class="kw">double rnd = u.GaussDistribution(class="num">0, -class="num">1, class="num">1, class="num">8); prob1 = u.Scale(a [i].f, minFF, maxFF, class="num">0, class="num">1); prob2 = u.Scale(a [k].f, minFF, maxFF, class="num">0, class="num">1); x = agent [i].cPrev [c] + rnd * (a [k].c [c] - agent [i].cPrev [c]) * (class="num">1 - prob1 - prob2); } a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } }
「把这条线请下神坛」
上面那段粒子位置更新公式,是整篇里最容易被神化的部分。原始写法用 (1 - prob1 - prob2) 做收缩系数,后来改成 (2 - prob1 - prob2),实质是把探索步长放大了一倍,让 agent[i] 更容易跳出 cPrev 附近的局部洼地。 在 MT5 里把这两行都留着、用注释切换,你能直接看到回测中种群离散度的变化:系数从 1 切到 2,前 200 代最优解波动标准差可能抬升 30%~50%,但陷在假极值的概率下降。外汇与贵金属波动跳变频繁,这类放大在高杠杆下属于高风险操作,参数乱动容易爆仓。 所以它只是优化器里的一行权重,不是圣杯。跑通了就调 prob1、prob2 看边界,跑不通就换收缩策略,别对着一条公式拜。
x = agent[i].cPrev[c] + rnd * (a[k].c[c] - agent[i].cPrev[c]) * (class="num">1 - prob1 - prob2); class=class="str">"cmt">//x = agent [i].cPrev [c] + rnd * (a [k].c [c] - agent [i].cPrev [c]) * (class="num">1 - prob1 - prob2); x = agent [i].cPrev [c] + rnd * (a [k].c [c] - agent [i].cPrev [c]) * (class="num">2 - prob1 - prob2);