老鹰策略(ES)·进阶篇
(2/3)· 当网格遍历卡在局部最优,鹰眼式全局-局部交替搜索可能帮你跳出陷阱
◍ 萤火虫算法的全局与局部搜索切换逻辑
这段初始化与移动代码实现了一个混合进化策略:先用 Lévy 飞行做全局探索,一旦当前最优适应度 fB 超过上轮记录 prevBestFitness,就切进以最优个体为中心的局部开发阶段。
初始化里 gamma_es 固定为 1.0,这是萤火虫算法吸收亮度的固定参数;种群每个个体坐标先在 [rangeMin, rangeMax] 内随机生成,再用 SeInDiSp 对齐到离散步长网格,避免无效浮点解。
Moving() 每次 epochCurrent 自增后做相位判断:不在局部阶段时跑 GlobalExploration(),若连续 5 代没提升(stagnationCounter>5),lambda 从当前值减 0.1 但不低于 1.0,让 Lévy 步长更激进。进入局部阶段后,有 0.8 概率调用 LocalExploitation(),用萤火虫相互吸引机制精搜。
在 MT5 里把 stagnationCounter 的阈值 5 和局部阶段触发概率 0.8 改成变量,能直接观察外汇品种参数空间陷入早熟的概率变化,贵金属与外汇均属高风险,回测结论仅代表历史样本倾向。
const class="type">class="kw">double &rangeStepP [], const class="type">int epochsP = class="num">0) { if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false; class=class="str">"cmt">//------------------------------------------------------------------ class=class="str">"cmt">// Initialize arrays ArrayResize(levyStep, coords); class=class="str">"cmt">// Initialize phase tracking inLocalSearchPhase = false; localSearchCenter = class="num">0; localSearchCounter = class="num">0; class=class="str">"cmt">// Initialize convergence tracking prevBestFitness = -DBL_MAX; stagnationCounter = class="num">0; class=class="str">"cmt">// Initialize epoch tracking epochMax = epochsP; epochCurrent = class="num">0; class=class="str">"cmt">// Fixed Firefly parameter gamma_es = class="num">1.0; class=class="str">"cmt">// Initialize the population randomly 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]); } a [i].f = -DBL_MAX; a [i].fB = -DBL_MAX; } class="kw">return true; } class="type">void C_AO_ES::Moving() { epochCurrent++; class=class="str">"cmt">// PHASE DECISION: Alternating between global and local search if (!inLocalSearchPhase) { class=class="str">"cmt">// PHASE class="num">1: GLOBAL EXPLORATION using Levy flights GlobalExploration(); class=class="str">"cmt">// Check whether it is necessary to class="kw">switch to local search class=class="str">"cmt">// Switch if we find a promising area(improving the best fitness) if (fB > prevBestFitness) { inLocalSearchPhase = true; localSearchCounter = class="num">0; prevBestFitness = fB; stagnationCounter = class="num">0; class=class="str">"cmt">// Find the best agent to center local search localSearchCenter = class="num">0; class="type">class="kw">double bestFit = -DBL_MAX; for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f > bestFit) { bestFit = a [i].f; localSearchCenter = i; } } } else { stagnationCounter++; class=class="str">"cmt">// In case of stagnation, increase exploration if (stagnationCounter > class="num">5) { lambda = MathMax(class="num">1.0, lambda - class="num">0.1); class=class="str">"cmt">// Make Levy flights more aggressive } } } else { if (u.RNDprobab() < class="num">0.8) { class=class="str">"cmt">// PHASE class="num">2: LOCAL EXPLOITATION using the Firefly algorithm LocalExploitation(); localSearchCounter++;
「局部收敛后如何切回全局搜索」
这段逻辑处理的是优化器在局部搜索迭代次数耗尽后的状态回退。当 localSearchCounter 达到 localIterations 时,把 inLocalSearchPhase 置为 false,并将 lambda 还原为 params[1].val 的初始值,避免局部参数污染下一轮全局探索。 Revision 函数负责更新个体历史最优与全局最优:若当前适应度 a[i].f 优于记录值 a[i].fB,则拷贝坐标数组;同样逻辑比对全局 fB,保证 cB 始终指向种群最佳位置。 全局探索阶段用 Levy 飞行更新坐标。stepScale 按进度自适应,初始为 0.01+0.2*(1-progress),epochCurrent 越接近 epochMax,步长越小,从大步长搜素收敛到精细搜素的概率倾向明显。边界用 SeInDiSp 做离散约束,防止越界。 局部开发阶段先筛出最佳解超球体内的代理,normalized_dist 累加各维度距离,为后续萤火虫算法靠拢做准备。外汇与贵金属参数优化属高风险操作,回测过拟合可能导致实盘失效。
class=class="str">"cmt">// Return to global search after local iterations are complete if (localSearchCounter >= localIterations) { inLocalSearchPhase = false; lambda = params [class="num">1].val; class=class="str">"cmt">// Reset lambda to its original value } } else { for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { if (u.RNDprobab() < class="num">0.5) { a [i].c [c] = cB [c]; } } } } } } class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class="type">void C_AO_ES::Revision() { for (class="type">int i = class="num">0; i < popSize; i++) { class=class="str">"cmt">// Updating the personal best one if (a [i].f > a [i].fB) { a [i].fB = a [i].f; ArrayCopy(a [i].cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } class=class="str">"cmt">// Update the global best one if (a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } } class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">// PHASE class="num">1: GLOBAL EXPLORATION using Levy flights class="type">void C_AO_ES::GlobalExploration() { for (class="type">int i = class="num">0; i < popSize; i++) { class=class="str">"cmt">// Generate Levy step GenerateLevyStep(); class=class="str">"cmt">// Update position using Levy flight for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double range = rangeMax [c] - rangeMin [c]; class=class="str">"cmt">// Adaptive step size depending on search progress class="type">class="kw">double progress = (epochMax > class="num">0) ? (class="type">class="kw">double)epochCurrent / (class="type">class="kw">double)epochMax : class="num">0.5; class="type">class="kw">double stepScale = class="num">0.01 + class="num">0.2 * (class="num">1.0 - progress); class=class="str">"cmt">// Start with big steps class=class="str">"cmt">// Apply the Levy step a [i].c [c] += levyStep [c] * range * stepScale; class=class="str">"cmt">// Boundary restrictions a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } } class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">// PHASE class="num">2: Local exploitation using the Firefly algorithm class="type">void C_AO_ES::LocalExploitation() { class=class="str">"cmt">// Identification of agents inside the hypersphere around the best solution class="type">class="kw">double agents_in_sphere []; ArrayResize(agents_in_sphere, class="num">0); for (class="type">int i = class="num">0; i < popSize; i++) { class="type">class="kw">double normalized_dist = class="num">0.0; for (class="type">int c = class="num">0; c < coords; c++) {
邻域过稀时的兜底取样
上面那段逻辑先用归一化欧氏距离圈出球形邻域里的个体,半径内或中心自身都会被收进 agents_in_sphere。若某次局部搜索中心周围太空,数组长度不足 5,就直接清空重来,改为对全部 popSize 个个体算距离再挑最近的。 兜底规则写得很直白:numAgents 取 5 与 popSize/3 的较大值,但绝不超过 popSize 本身。也就是说种群小于 15 时只补到 5 个,大于等于 15 时按约 33% 比例抽近邻,避免局部搜索退化成单点抖动。 选人过程是个朴素的最小距离扫描:每轮从还没入选的索引里挑 distances 最小的填进数组,时间复杂度约 O(numAgents * popSize),种群上万时在 MT5 里可能吃掉可观的 tick 时间。实盘前建议把 popSize/3 改成可配置参数,黄金盘面跳空多,邻域过稀会比欧美对更频繁触发这条分支。
class="type">class="kw">double diff = (a [i].c [c] - a [localSearchCenter].c [c]) / (rangeMax [c] - rangeMin [c]); normalized_dist += diff * diff; } normalized_dist = MathSqrt(normalized_dist); class=class="str">"cmt">// Include agents inside the sphere or the center itself if (normalized_dist <= sphereRadius || i == localSearchCenter) { class="type">int size = ArraySize(agents_in_sphere); ArrayResize(agents_in_sphere, size + class="num">1); agents_in_sphere [size] = i; } } class=class="str">"cmt">// If there are too few agents, expand to the nearest neighbors if (ArraySize(agents_in_sphere) < class="num">5) { ArrayResize(agents_in_sphere, class="num">0); class=class="str">"cmt">// Calculate distances for all agents class="type">class="kw">double distances []; ArrayResize(distances, popSize); for (class="type">int i = class="num">0; i < popSize; i++) { if (i == localSearchCenter) { distances [i] = class="num">0.0; } else { class="type">class="kw">double dist = class="num">0.0; for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double diff = (a [i].c [c] - a [localSearchCenter].c [c]) / (rangeMax [c] - rangeMin [c]); dist += diff * diff; } distances [i] = MathSqrt(dist); } } class=class="str">"cmt">// Take the closest class="num">5 agents or class="num">30% of the population class="type">int numAgents = MathMin(popSize, MathMax(class="num">5, popSize / class="num">3)); ArrayResize(agents_in_sphere, numAgents); class=class="str">"cmt">// Simple selection of nearby agents for (class="type">int k = class="num">0; k < numAgents; k++) { class="type">class="kw">double minDist = DBL_MAX; class="type">int minIdx = -class="num">1; for (class="type">int i = class="num">0; i < popSize; i++) { class="type">bool already_selected = false; for (class="type">int j = class="num">0; j < k; j++) { if (agents_in_sphere [j] == i) { already_selected = true; break; } } if (!already_selected && distances [i] < minDist) { minDist = distances [i]; minIdx = i; } } agents_in_sphere [k] = minIdx; } } class=class="str">"cmt">// Execute the Firefly algorithm on the selected agents class="type">int numLocalAgents = ArraySize(agents_in_sphere); for (class="type">int i = class="num">0; i < numLocalAgents; i++) { class="type">int idx_i = (class="type">int)agents_in_sphere [i];
◍ 萤火虫吸引与莱维步长的代码落地
这段逻辑把「邻域里更亮的个体吸引当前个体」和「莱维飞行扰动」两块拆开了写,直接对应 EA 优化器的核心迭代。 先说吸引部分:外层遍历本地智能体,跳过自身后取对方索引 idx_j,仅当对方适应度 a[idx_j].f 大于自身时才发生位移。距离用各维归一化差的平方和 r_squared 表示,再套 beta = beta0 * exp(-gamma_es * r_squared) 算吸引力——离得越远吸引力衰减越快,这是典型萤火虫算法设定。 位移方程里 a[idx_i].c[c] += beta*(目标-自身) + alpha*随机.uniform(-0.5,0.5)*区间*0.1,后半段是随机扰动,最后用 SeInDiSp 把坐标夹回 [rangeMin, rangeMax] 并量化到 step。开 MT5 把 gamma_es 从 0.5 调到 2.0,能明显看到收敛半径变窄。 莱维步长走 Mantegna 近似:sigma = (Γ(1+λ)·sin(πλ/2) / (Γ((1+λ)/2)·λ·2^((λ-1)/2)))^(1/λ),再用两个高斯采样 u_val、v_val 算 levyStep[c]=u_val/v_val^(1/λ)。代码把步长硬截断在 ±10.0,避免极端跳跃把参数搜飞。 外汇与贵金属品种参数空间崎岖,这类算法可能陷入局部谷,实盘前请用历史数据回测验证稳定性。
for (class="type">int j = class="num">0; j < numLocalAgents; j++) { if (i == j) class="kw">continue; class="type">int idx_j = (class="type">int)agents_in_sphere [j]; class=class="str">"cmt">// If agent j is better than agent i, move i to j if (a [idx_j].f > a [idx_i].f) { class=class="str">"cmt">// Calculate the distance class="type">class="kw">double r_squared = class="num">0.0; for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double diff = (a [idx_j].c [c] - a [idx_i].c [c]) / (rangeMax [c] - rangeMin [c]); r_squared += diff * diff; } class=class="str">"cmt">// Calculate attractiveness class="type">class="kw">double beta = beta0 * MathExp(-gamma_es * r_squared); class=class="str">"cmt">// Move agent i to agent j for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double range = rangeMax [c] - rangeMin [c]; class=class="str">"cmt">// Firefly motion equation a [idx_i].c [c] += beta * (a [idx_j].c [c] - a [idx_i].c [c]) + alpha * (u.RNDfromCI(-class="num">0.5, class="num">0.5)) * range * class="num">0.1; class=class="str">"cmt">// Apply borders a [idx_i].c [c] = u.SeInDiSp(a [idx_i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } } } class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Generate Levy step using Mantegna algorithm class="type">void C_AO_ES::GenerateLevyStep() { class=class="str">"cmt">// Calculate sigma for Mantegna algorithm class="type">class="kw">double numerator = Gamma(class="num">1.0 + lambda) * MathSin(M_PI * lambda / class="num">2.0); class="type">class="kw">double denominator = Gamma((class="num">1.0 + lambda) / class="num">2.0) * lambda * MathPow(class="num">2.0, (lambda - class="num">1.0) / class="num">2.0); class="type">class="kw">double sigma = MathPow(numerator / denominator, class="num">1.0 / lambda); for (class="type">int c = class="num">0; c < coords; c++) { class=class="str">"cmt">// Generate u and v from normal distributions class="type">class="kw">double u_val = GenerateGaussian() * sigma; class="type">class="kw">double v_val = MathAbs(GenerateGaussian()); class=class="str">"cmt">// Calculate the Levy step if (v_val > class="num">1e-10) { levyStep [c] = u_val / MathPow(v_val, class="num">1.0 / lambda); } else { levyStep [c] = class="num">0.0; } class=class="str">"cmt">// Limit extreme values if (MathAbs(levyStep [c]) > class="num">10.0) { levyStep [c] = class="num">10.0 * (levyStep [c] > class="num">0 ? class="num">1.0 : -class="num">1.0); } } }
「高斯与伽马函数的底层采样实现」
在 MT5 里做统计型指标,最常被卡住的就是随机数与特殊函数的来源。下面这两段直接给了可编译的取法,复制进 EA 或脚本就能跑。 GenerateGaussian 用 Box-Muller 变换出标准正态随机数。它靠两个均匀随机数 u_val、v_val(都从 0~1 区间取),通过 mag = sqrt(-2*ln(u)) 和角度 2πv 映射成独立正态样本;用 static hasSpare 缓存另一个分量,下次调用直接返回,省掉一半计算。注意 u_val 加了 1e-10 避免 ln(0) 报错。 Gamma 函数走 Lanczos 近似,g 取 7.0、系数数组长度 9。z<0.5 时走反射公式递归,否则减 1 后累加系数,最后用 sqrt(2π)·t^(z+0.5)·e^(-t)·x 出值。这套系数在 z>0.5 时相对误差通常在 1e-10 量级,够价格分布拟合用。 外汇与贵金属波动聚集明显,用这类采样做蒙特卡洛压力测试时,样本量低于 1e4 可能给出偏薄的尾部分布,建议先在策略测试器里拉 5e4 次对照直方图再上实盘逻辑。
class=class="str">"cmt">// Generate a Gaussian random number using the Box-Muller transform class="type">class="kw">double C_AO_ES::GenerateGaussian() { class="kw">static class="type">bool hasSpare = false; class="kw">static class="type">class="kw">double spare; if (hasSpare) { hasSpare = false; class="kw">return spare; } hasSpare = true; class="type">class="kw">double u_val = u.RNDfromCI(class="num">0.0, class="num">1.0); class="type">class="kw">double v_val = u.RNDfromCI(class="num">0.0, class="num">1.0); class="type">class="kw">double mag = MathSqrt(-class="num">2.0 * MathLog(u_val + class="num">1e-10)); spare = mag * MathCos(class="num">2.0 * M_PI * v_val); class="kw">return mag * MathSin(class="num">2.0 * M_PI * v_val); } class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Approximation of the gamma function using Lanczos approximation class="type">class="kw">double C_AO_ES::Gamma(class="type">class="kw">double z) { if (z < class="num">0.5) { class=class="str">"cmt">// Reflection formula for z < class="num">0.5 class="kw">return M_PI / (MathSin(M_PI * z) * Gamma(class="num">1.0 - z)); } class=class="str">"cmt">// Lanczos coefficients const class="type">class="kw">double g = class="num">7.0; class="type">class="kw">double coef [] = { class="num">0.99999999999980993, class="num">676.5203681218851, -class="num">1259.1392167224028, class="num">771.32342877765313, -class="num">176.61502916214059, class="num">12.507343278686905, -class="num">0.13857109526572012, class="num">9.9843695780195716e-6, class="num">1.5056327351493116e-7 }; z -= class="num">1.0; class="type">class="kw">double x = coef [class="num">0]; for (class="type">int i = class="num">1; i < class="num">9; i++) { x += coef [i] / (z + i); } class="type">class="kw">double t = z + g + class="num">0.5; class="type">class="kw">double sqrt2pi = MathSqrt(class="num">2.0 * M_PI); class="kw">return sqrt2pi * MathPow(t, z + class="num">0.5) * MathExp(-t) * x; } class=class="str">"cmt">//————————————————————————————————————————————————————————————————————