种群优化算法:鸟群算法(BSA)·进阶篇
◍ 鸟群算法代理类的参数与接口
这套鸟群搜索(BSA)代理类用 10 个外部参数驱动群体行为,从飞行概率到生产者/乞食者权重都在 params 数组里读取,索引 1 到 10 分别对应 flyingProb、producerProb、foragingProb、a1、a2、C、S、FL、producerPower、scroungerPower。 a1、a2、FL 三个常量被限定在 [0...2] 区间,C 与 S 是认知/社会系数,直接影响个体向历史最优与群体均值靠拢的拉力。生产者和乞食者概率则决定每次迭代中两种角色的分布比例。 Init 接口接收三组区间数组(最小/最大/步长)加可选 epoch 数,Moving、Revision、Injection 分别负责位移、参数修订与单点注入。私有段里的 mean[] 存全群平均位置,BirdProducer / BirdScrounger / BirdForaging / BirdVigilance 是四类微观行为的具体实现。 在 MT5 里把这类代理接进 EA 时,先确认 params[4]~params[6] 不要越出注释标定的常数域,否则群体可能过早收敛或发散。外汇与贵金属市场波动剧烈,任何基于群智能的仓位信号都只是概率倾向,实盘前务必用历史数据做样本外验证。
flyingProb = params [class="num">1].val; producerProb = params [class="num">2].val; foragingProb = params [class="num">3].val; a1 = params [class="num">4].val; a2 = params [class="num">5].val; C = params [class="num">6].val; S = params [class="num">7].val; FL = params [class="num">8].val; producerPower = params [class="num">9].val; scroungerPower = params [class="num">10].val; } class="type">bool Init(const class="type">class="kw">double &rangeMinP [], class=class="str">"cmt">//minimum search range const class="type">class="kw">double &rangeMaxP [], class=class="str">"cmt">//maximum search range const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search const class="type">int epochsP = class="num">0); class=class="str">"cmt">//number of epochs class="type">void Moving(); class="type">void Revision(); class="type">void Injection(const class="type">int popPos, const class="type">int coordPos, const class="type">class="kw">double value); class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">class="kw">double flyingProb; class=class="str">"cmt">//Flight probability class="type">class="kw">double producerProb; class=class="str">"cmt">//Producer probability class="type">class="kw">double foragingProb; class=class="str">"cmt">//Foraging probability class="type">class="kw">double a1; class=class="str">"cmt">//a1 constant [class="num">0...class="num">2] class="type">class="kw">double a2; class=class="str">"cmt">//a2 constant [class="num">0...class="num">2] class="type">class="kw">double C; class=class="str">"cmt">//Cognitive coefficient class="type">class="kw">double S; class=class="str">"cmt">//Social coefficient class="type">class="kw">double FL; class=class="str">"cmt">//FL constant [class="num">0...class="num">2] class="type">class="kw">double producerPower; class=class="str">"cmt">//Producer power class="type">class="kw">double scroungerPower; class=class="str">"cmt">//Scrounger power S_BSA_Agent agent []; class="kw">private: class=class="str">"cmt">//------------------------------------------------------------------- class="type">class="kw">double mean []; class=class="str">"cmt">//represents the element of the average position of the whole bird’s swarm class="type">class="kw">double N; class="type">class="kw">double e; class=class="str">"cmt">//epsilon class="type">void BirdProducer(class="type">int pos); class="type">void BirdScrounger(class="type">int pos); class="type">void BirdForaging(class="type">int pos); class="type">void BirdVigilance(class="type">int pos); }; class="type">bool C_AO_BSA::Init(const class="type">class="kw">double &rangeMinP [], class=class="str">"cmt">//minimum search range
鸟群算法里个体怎么动起来
这段 MQL5 实现了一个基于鸟群行为(BSA)的群体优化类移动逻辑,核心是把每只「鸟」按概率分到飞行 / 觅食两类状态,再细拆成生产者、乞食者、觅食、警戒四种微观行为。 初始化时 StandardInit 先校验参数区间,ArrayResize 按 popSize 开 agent 数组,每只 agent 用 Init(coords) 铺好维度;N 记种群规模,e 置为 DBL_MIN 表示尚未评估出最优适应度。 Moving() 里的 revision 标志很关键:首轮不进入常规飞行,而是给全部个体在 [rangeMin, rangeMax] 内均匀随机并吸附到离散步长 rangeStep,相当于种群播种,之后 revision 置 true 才走正常迭代。 正常迭代对每个个体先掷 RNDprobab() 判断飞不飞:飞且掷出小于 producerProb 就 BirdProducer,否则 BirdScrounger 跟随;不飞则按 foragingProb 分流到 BirdForaging 或 BirdVigilance。四个概率参数(flyingProb / producerProb / foragingProb 及隐含的警戒补集)直接决定搜索的发散与收敛倾向,外汇与贵金属参数寻优中调这三值大概率会改变过拟合程度,属高风险实验。 BirdProducer 对第 pos 个个体的每个坐标用 GaussDistribution 做高斯扰动,幅度由 producerPower 控制,再经 SeInDiSp 夹回连续区间并离散化。想验证就开 MT5 把 producerPower 从 0.1 调到 1.0,看 EA 优化器在 EURUSD 15M 上调均幅参数时收敛速度的差异。
const class="type">class="kw">double &rangeMaxP [], class=class="str">"cmt">//maximum search range const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search const class="type">int epochsP = class="num">0) class=class="str">"cmt">//number of epochs { if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return class="kw">false; class=class="str">"cmt">//---------------------------------------------------------------------------- ArrayResize(agent, popSize); for (class="type">int i = class="num">0; i < popSize; i++) agent [i].Init(coords); ArrayResize(mean, coords); N = popSize; e = DBL_MIN; class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_BSA::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]); a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } revision = true; class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) { class=class="str">"cmt">//bird is flying------------------------------------------------------------ if (u.RNDprobab() < flyingProb) { class=class="str">"cmt">//bird producer if (u.RNDprobab() < producerProb) BirdProducer(i); class=class="str">"cmt">//bird is looking for a new place to eat class=class="str">"cmt">//bird is not a producer else BirdScrounger(i); class=class="str">"cmt">//scrounger follows the producer } class=class="str">"cmt">//bird is not flying-------------------------------------------------------- else { class=class="str">"cmt">//bird foraging if (u.RNDprobab() < foragingProb) BirdForaging(i); class=class="str">"cmt">//bird feeds class=class="str">"cmt">//bird is not foraging else BirdVigilance(i); class=class="str">"cmt">//bird vigilance } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_BSA::BirdProducer(class="type">int pos) { class="type">class="kw">double x = class="num">0.0; class=class="str">"cmt">//bird position for (class="type">int c = class="num">0; c < coords; c++) { x = a [pos].c [c]; x = u.GaussDistribution(x, rangeMin [c], rangeMax [c], producerPower); a [pos].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
「鸟群觅食三阶段的代码骨架」
这套智能优化把每只「鸟」当成一组待调参数,在 coords 个维度上更新位置。下面三段是核心迭代逻辑,直接丢进 MT5 的 EA 源码里就能跑通编译。 BirdScrounger 做「偷食」:随机挑一只非自身的鸟 K,用它的历史最优 xK 与自身最优 x 做差,乘上 FL 和带 scroungerPower 的高斯扰动,再夹回参数边界。高斯分布调用 u.GaussDistribution(0, -1.0, 1.0, scroungerPower),意味着均值 0、下限 -1、上限 1,尾宽由 scroungerPower 控制。 BirdForaging 是标准觅食:x 朝自身最优 p 拉 C*r1,朝全局最优 g 拉 S*r2。r1、r2 都是 [0,1] 均匀随机数,C 和 S 是固定步长系数,改这两个值会明显改变收敛速度。 BirdVigilance 算「警惕」:先累加全群 fBest 得 sumFit,再对每维求群体均值 mean[c],随机选 K 后取双方适应度 pFit 与 pFitK。A1、A2 两个权重留到后面算移动方向,这里只完成了数据准备。外汇与贵金属参数优化属高风险操作,回测盈利不代表实盘概率占优。 想验证行为差异,把 FL 从 0.5 调到 2.0、scroungerPower 从 1.0 降到 0.3,同品种 EURUSD M15 上大概率看到收敛轮次缩短但过拟合倾向上升。
class="type">void C_AO_BSA::BirdScrounger(class="type">int pos) { class="type">int K = class="num">0; class=class="str">"cmt">//position of a randomly selected bird in a swarm class="type">class="kw">double x = class="num">0.0; class=class="str">"cmt">//best bird position class="type">class="kw">double xK = class="num">0.0; class=class="str">"cmt">//current best position of a randomly selected bird in a swarm for (class="type">int c = class="num">0; c < coords; c++) { do K = u.RNDminusOne(popSize); class="kw">while (K == pos); x = agent [pos].cBest [c]; xK = agent [K].cBest [c]; x = x + (xK - x) * FL * u.GaussDistribution(class="num">0, -class="num">1.0, class="num">1.0, scroungerPower); a [pos].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } class="type">void C_AO_BSA::BirdForaging(class="type">int pos) { class="type">class="kw">double x = class="num">0.0; class=class="str">"cmt">//current bird position class="type">class="kw">double p = class="num">0.0; class=class="str">"cmt">//best bird position class="type">class="kw">double g = class="num">0.0; class=class="str">"cmt">//best global position class="type">class="kw">double r1 = class="num">0.0; class=class="str">"cmt">//uniform random number [class="num">0.0 ... class="num">1.0] class="type">class="kw">double r2 = class="num">0.0; class=class="str">"cmt">//uniform random number [class="num">0.0 ... class="num">1.0] for (class="type">int c = class="num">0; c < coords; c++) { x = a [pos].c [c]; p = agent [pos].cBest [c]; g = cB [c]; r1 = u.RNDprobab(); r2 = u.RNDprobab(); x = x + (p - x) * C * r1 + (g - x) * S * r2; a [pos].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } class="type">void C_AO_BSA::BirdVigilance(class="type">int pos) { class="type">int K = class="num">0; class=class="str">"cmt">//position of a randomly selected bird in a swarm class="type">class="kw">double sumFit = class="num">0.0; class=class="str">"cmt">//best birds fitness sum class="type">class="kw">double pFitK = class="num">0.0; class=class="str">"cmt">//best fitness of a randomly selected bird class="type">class="kw">double pFit = class="num">0.0; class=class="str">"cmt">//best bird fitness class="type">class="kw">double A1 = class="num">0.0; class="type">class="kw">double A2 = class="num">0.0; class="type">class="kw">double r1 = class="num">0.0; class=class="str">"cmt">//uniform random number [ class="num">0.0 ... class="num">1.0] class="type">class="kw">double r2 = class="num">0.0; class=class="str">"cmt">//uniform random number [-class="num">1.0 ... class="num">1.0] class="type">class="kw">double x = class="num">0.0; class=class="str">"cmt">//best bird position class="type">class="kw">double xK = class="num">0.0; class=class="str">"cmt">//best position of a randomly selected bird in a swarm ArrayInitialize(mean, class="num">0.0); for (class="type">int i = class="num">0; i < popSize; i++) sumFit += agent [i].fBest; for (class="type">int c = class="num">0; c < coords; c++) { for (class="type">int i = class="num">0; i < popSize; i++) mean [c] += a [i].c [c]; mean [c] /= popSize; } do K = u.RNDminusOne(popSize); class="kw">while (K == pos); pFit = agent [pos].fBest;
◍ 群体最优与个体历史的更新逻辑
这段代码片段承接前文的位置更新计算,核心在做两件事:用 Revision 函数刷新全局最差替补与个体历史最优。先扫一遍种群,把适应度超过 fB 的个体下标记下来,若有则把该个体的坐标数组 cB 整体拷贝走,fB 同步更新。 紧接着的第二层循环只关心“自己有没有比过去强”:当临时个体 a[i].f 大于 agent[i].fBest 时,就把本次适应度存进 fBest,并将当前坐标数组覆盖进 cBest。这里用的是 ArrayCopy 配 WHOLE_ARRAY,意味着维度必须预先对齐,否则拷贝长度会出错。 回到位置更新段,A1 与 A2 两个衰减系数分别由种群总和 sumFit 与迭代计数 N 驱动,exp 里的分母都加了极小量 e 防零除。坐标循环内 r1 取自概率随机、r2 取自 [-1,1] 均匀随机,新坐标 x 由均值吸引项和 K 个体吸引项叠加,最后经 SeInDiSp 钳制到 [rangeMin, rangeMax] 并按 rangeStep 离散化。 在 MT5 里跑这套,建议先打印前 50 代 sumFit 与 fB 的走势,若 fB 长期不降,说明选择压偏弱,可尝试调小 a2 看收敛是否变快。外汇与贵金属行情高波动,此类优化仅用于参数搜索,实盘信号仍可能失效。
pFitK = agent [K].fBest; A1 = a1 * exp(-pFit * N / (sumFit + e)); A2 = a2 * exp(((pFit - pFitK) / (fabs(pFitK - pFit) + e)) * (N * pFitK / (sumFit + e))); for (class="type">int c = class="num">0; c < coords; c++) { r1 = u.RNDprobab(); r2 = u.RNDfromCI(-class="num">1, class="num">1); x = agent [pos].cBest [c]; xK = agent [K].cBest [c]; x = x + A1 * (mean [c] - x) * r1 + A2 * (xK - x) * r2; a [pos].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_BSA::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) ind = i; } if (ind != -class="num">1) { fB = a [ind].f; 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].fBest) { agent [i].fBest = a [i].f; ArrayCopy(agent [i].cBest, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
鸟群算法跑分:高维平滑面直接垫底
把鸟群算法(BSA)丢进 33 个优化算法的横向评测表里,总得分 5.055,占满分的 56.17%,排第 7。对比榜首二进制遗传算法的 6.921(76.90%),差距不算离谱,但在变量膨胀的平滑 Hilly 函数上,BSA 的 500 维结果只有 0.25767,直接跌到全部算法最后一名。 具体看三类测试面:Hilly 的 5/25/500 维得分 0.90857 / 0.73661 / 0.25767;Forest 为 0.90437 / 0.81619 / 0.16401;Megacity 离散函数则是 0.61692 / 0.54154 / 0.10951。每类都跑满 10000 次函数调用,维度一上去,BSA 的收敛质量就塌方。 可视化里能明显看到,BSA 会覆盖局部表面,但也可能陷进局部陷阱,全局最优的稳定性存疑。不过在森林类和离散 Megacity 上,它还是压过了评分更高的 (P+O)ES 和 SDSm 的部分维度结果,说明不是全盘拉胯。 外汇与贵金属参数优化属高风险场景,回测得分不直接等于实盘收益,MT5 里接 BSA 做输入参数寻优时,建议先锁低维、再放高维,别一上来就喂 500 变量。
「画得少,看得清」
把 BSA 丢进 MT5 标准测试器的数学计算模式,用自写的进化 EA 跑参数寻优,最终在公开评级里拿到第 7 名;这成绩建立在 4 种鸟类行为序列组合之上,换组合顺序仍有往上挤的空间。 图 2 把颜色等级 ≥0.99 的结果标白,图 3 的直方图标尺 0–100,100 是理论极值——前 5 名算法在 50 人口、10 参数设定下,约第 100 个epoch就贴近天花板,第 50 个epoch已到 70–80%,步长为 0 时如此,步长非零收敛更狠。 尖锐森林函数和高维离散模型是 BSA 的强项,但平滑函数如 Hilly 上扩展性差、实现复杂、收敛偏低。外汇与贵金属市场高风险,这类优化器只解决数学寻参,不直接给出交易信号,真要接实盘得自己过桥。 附带的 BSA.zip(28.34 KB)是当下代码版,作者不担保与标准描述绝对一致,多处已为搜索力改过。开 MT5 把 zip 里的 EA 挂上数学模式,先复现第 7 名再动行为序列,比空谈鸟群有用。