交易所市场算法(EMA)·进阶篇
(2/3)· 当精英、中产、新手三组交易者在均衡与波动中切换策略,全局最优解如何被逼近
均衡阶段的三组位置更新逻辑
均衡市场阶段把种群切成了 G₁、G₂、G₃ 三组,各自按不同规则向领导者靠拢。G₁ 作为已定位的领导者集合,本阶段位置保持不动,只输出坐标供其他组参考。 G₂ 中每个个体 i 会随机挑一位 G₁ 里的领导者 k,按 xᵢ = xᵢ + rand() × r₁ᵃᵈᵃᵖᵗ × (xₖ - xᵢ) 做一步偏移。rand() 是 [0,1) 均匀随机数,r₁ᵃᵈᵃᵖᵗ 为自适应半径,决定单次跟随的跨度,MT5 策略测试器里把该系数从 0.5 调到 1.2 时,群体收敛速度可能明显变化。 G₃ 的更新更激进:个体 i 同时参考 G₁ 的领导者 k₁ 与 G₂ 的领导者 k₂,公式为 xᵢ = xᵢ + rand() × r₁ᵃᵈᵃᵖᵗ × (xₖ₁ - xᵢ) + rand() × r₂ᵃᵈᵃᵖᵗ × (xₖ₂ - xᵢ)。双随机项叠加让 G₃ 在均衡期仍保留较大探索扰动,外汇与贵金属行情高波动下这种结构可能延缓早熟但也会增加回撤风险。
◍ 波动市里的群体扰动规则
在波动市场阶段,算法把种群拆成 G₂ 与 G₃ 两组分别施加位置扰动,目的是让搜索避开平稳期的局部黏滞。G₂ 组内每个个体以 50% 概率直接拷贝当前全局最优 xᵇᵉˢᵗ,其余情况则锚定 G₁ 的中心位,叠加 noise×α×(1−decay×0.5) 的偏移量;这里的 decay×0.5 意味着随迭代衰减,后期扰动幅度会自然收敛。 G₃ 的处理更激进:以概率 α 直接随机重置到 random_position(),或以 0.5+α/2 的概率施加 big_disturbance 大跳变,剩下个体则按 2xᵢ−center(worst)+small_noise 做反向最差中心逃逸。实盘映射上,这套机制等价于在波动率放大时强制一部分仓位脱离原结构,防止群体陷在假突破区间。 把 G₂ 与 G₃ 的新位置写回种群后,立即重算全局最优。MT5 上验证时建议把 α 初值设 0.3、decay 设 0.05,观察波动段种群分散度是否比平稳段高 20%–40%,可作为调参基线。外汇与贵金属波动阶段杠杆风险极高,任何参数优势都只是概率倾向,不构成方向保证。
「EMA优化类的代理分组与移动逻辑」
C_AO_EMA 继承自 C_AO,把种群切成三等份来跑 Exchange Market Algorithm。默认 popSize=60,因此每组 groupSize 实际为 20;r1=1.5 管第二组吸收强度,r2=0.8 管第三组,riskAlpha=0.3 决定波动阶段敢不敢乱跳。 Init 接收 rangeMin、rangeMax、rangeStep 与 epochs,先校验边界,再把 tempPop 按 popSize 开好内存,用 coords 初始化临时代理。返回 true 才代表能进 Moving。 Moving 首次跑会随机撒点并按步长对齐,之后每代 currentEpoch 自增,decayRate 用 S 型曲线从 0 滑到 1。第一阶段均衡市场:0~19 号精英不动;20~39 号挑精英随机领导者按 r1×decayRate 偏移;40~59 号挑两位领导者双向吸。第二阶段波动:二组 50% 概率直接抄 cB,否则绕精英中心加噪;三组以 0.3 概率全随机重初始化,否则广搜或对立学习。 GetDecayRate 用 1/(1+exp(-10*(progress-0.5))) 算衰减,progress 过 0.5 时曲线最陡,前期重探索后期重利用。Revision 把主种群按 f 排序,a[0] 拷进 cB、f 值给 fB,静态数组 aT 只建一次。 别把 riskAlpha 调满 riskAlpha 提到 0.8 以上,三组每代近八成概率全随机重初始化,外汇 15 分钟周期参数寻优会长时间不收敛;贵金属跳空行情下更易跑飞,建议先在策略测试器用 0.2~0.4 区间验证。
class=class="str">"cmt">//———————————————————————————————————————————————————————————————————— class C_AO_EMA : class="kw">public C_AO { class="kw">public: class=class="str">"cmt">//---------------------------------------------------------- ~C_AO_EMA() { } C_AO_EMA() { ao_name = "EMA"; ao_desc = "Exchange Market Algorithm"; ao_link = "[MQL5官方文档] popSize = class="num">60; class=class="str">"cmt">// Population size r1 = class="num">1.5; class=class="str">"cmt">// Absorption coefficient for group class="num">2 r2 = class="num">0.8; class=class="str">"cmt">// Absorption coefficient for group class="num">3 riskAlpha = class="num">0.3; class=class="str">"cmt">// Risk factor ArrayResize(params, class="num">4); params [class="num">0].name = "popSize"; params [class="num">0].val = popSize; params [class="num">1].name = "r1"; params [class="num">1].val = r1; params [class="num">2].name = "r2"; params [class="num">2].val = r2; params [class="num">3].name = "riskAlpha"; params [class="num">3].val = riskAlpha; } class="type">void SetParams() { popSize = (class="type">int)params [class="num">0].val;
参数越界兜底与群体初始化
在基于 AO-EMA 的优化器里,r1、r2 和 riskAlpha 三个系数直接从外部参数数组取数,但取完必须做一道越界兜底。popSize 若小于 6 会被强制拉回 6,且必须被 3 整除——因为后续要把种群均分成三组,每组 size = popSize/3,非 3 倍数会导致分组越界。 r1 与 r2 作为吸收系数只允许非负,riskAlpha 作为风险因子被夹在 [0.0, 1.0] 区间,超出则截断。这套硬约束能保证算法在 MT5 实盘加载时不会因为乱填参数直接崩初始化。 Init 函数负责把临时种群 tempPop 按 popSize 扩容,并逐个调用 S_AO_Agent 的 Init(coords) 完成坐标初始化。currentEpoch 归零、totalEpochs 接收传入的迭代轮数,groupSize 锁定为 popSize/3——例如 popSize=30 时每组恰好 10 个 agent。 [CODE] r1 = params [1].val; // 从参数数组第2项取吸收系数1 r2 = params [2].val; // 从参数数组第3项取吸收系数2 riskAlpha = params [3].val; // 从参数数组第4项取风险因子 // Check the parameters validity if (popSize < 6) popSize = 6; // 种群规模下限保护,低于6强制设为6 if (popSize % 3 != 0) popSize = ((popSize / 3) + 1) * 3; // Multiple of 3 // 种群规模必须被3整除,向上取整到最近的3倍数 if (r1 < 0.0) r1 = 0.0; // 吸收系数1非负截断 if (r2 < 0.0) r2 = 0.0; // 吸收系数2非负截断 if (riskAlpha < 0.0) riskAlpha = 0.0; // 风险因子下限截断 if (riskAlpha > 1.0) riskAlpha = 1.0; // 风险因子上限截断 } bool Init (const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0); // 声明初始化接口,接收搜索空间上下界、步长与迭代轮数 void Moving (); // 声明主循环移动函数 void Revision (); // 声明修正函数 double r1; // Absorption coefficient 1 // 吸收系数1成员 double r2; // Absorption coefficient 2 // 吸收系数2成员 double riskAlpha; // Risk factor // 风险因子成员 private: //--------------------------------------------------------- int groupSize; // size of each group (popSize/3) // 每组规模私有成员 int currentEpoch; // current epoch // 当前迭代轮次 int totalEpochs; // total number of epochs // 总迭代轮次 S_AO_Agent tempPop []; // temporary population for storing new positions // 临时种群容器 double GetDecayRate (); // 衰减率获取函数声明 }; bool C_AO_EMA::Init (const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0) { if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false; // 标准初始化失败则直接返回 groupSize = popSize / 3; // 每组规模=总种群/3 currentEpoch = 0; // 当前轮次归零 totalEpochs = epochsP; // 总轮次赋值 ArrayResize (tempPop, popSize); // 临时种群按种群规模扩容 for (int i = 0; i < popSize; i++) // 遍历每个agent { tempPop [i].Init (coords); // 调用单体初始化注入坐标 } return true; // 初始化成功 } void C_AO_EMA::Moving () {
◍ 群体智能里的吸收与搜索两阶段
这段逻辑把种群迭代拆成首次初始化和后续演化两步。首次跑的时候 revision 为 false,直接给 popSize 个个体在每个坐标维度上用 RNDfromCI 撒点,再用 SeInDiSp 吸附到合法离散网格,然后置 revision=true 并 return,避免重复初始化。 后续每次调用先 currentEpoch++,按 GetDecayRate() 拿衰减率。先把当前种群 a 整体拷到 tempPop 作为暂存,精英组(0 到 groupSize-1)原样不动,这是很多外汇参数寻优里保住历史最优解的常见手法。 PHASE 1 是平衡市假设下的吸收算子。组2(groupSize 到 2*groupSize-1)用 adaptiveR1 = r1*(1-0.5*decayRate) 向随机精英靠拢;组3 再用 r1*(1-0.3*decayRate) 同时参考精英组和组2两个领导,权重叠加。注意衰减率越高,吸收步长收缩越明显,行情从震荡转趋势时可能漏掉远端解。 PHASE 2 转入波动市搜索。组2 有 50% 概率直接跳到 cB[c] 中心位,否则围绕精英中心做局部扰动。MT5 上把 groupSize 设成 popSize 的 10%~20%、decayRate 初始 0.1 附近,能直观看到前两阶段种群散布差异。贵金属与外汇杠杆交易高风险,上述参数仅作代码验证参考,不预示任何收益。
class=class="str">"cmt">// Initial setup 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; } currentEpoch++; class="type">class="kw">double decayRate = GetDecayRate(); class=class="str">"cmt">// Copy the current population to the temporary one for (class="type">int i = class="num">0; i < popSize; i++) { ArrayCopy(tempPop [i].c, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); tempPop [i].f = a [i].f; } class=class="str">"cmt">// PHASE class="num">1: Balanced market(absorbing operators) class=class="str">"cmt">// Group class="num">1 (elite) - does not change class=class="str">"cmt">// Indices: class="num">0 ... groupSize-class="num">1 class=class="str">"cmt">// Group class="num">2 - absorbing class="kw">operator class="num">1 class=class="str">"cmt">// Indices: groupSize ... class="num">2*groupSize-class="num">1 class="type">class="kw">double adaptiveR1 = r1 * (class="num">1.0 - decayRate * class="num">0.5); for (class="type">int i = groupSize; i < class="num">2 * groupSize; i++) { class=class="str">"cmt">// Each agent in group class="num">2 selects a random leader from group class="num">1 class="type">int leaderIdx = u.RNDminusOne(groupSize); for (class="type">int c = class="num">0; c < coords; c++) { tempPop [i].c [c] = a [i].c [c] + u.RNDprobab() * adaptiveR1 * (a [leaderIdx].c [c] - a [i].c [c]); tempPop [i].c [c] = u.SeInDiSp(tempPop [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">// Group class="num">3 - absorbing class="kw">operator class="num">2 class=class="str">"cmt">// Indices: class="num">2*groupSize ... popSize-class="num">1 adaptiveR1 = r1 * (class="num">1.0 - decayRate * class="num">0.3); for (class="type">int i = class="num">2 * groupSize; i < popSize; i++) { class=class="str">"cmt">// Selection of leaders from groups class="num">1 and class="num">2 class="type">int leader1Idx = u.RNDminusOne(groupSize); class="type">int leader2Idx = groupSize + u.RNDminusOne(groupSize); for (class="type">int c = class="num">0; c < coords; c++) { tempPop [i].c [c] = a [i].c [c] + u.RNDprobab() * adaptiveR1 * (a [leader1Idx].c [c] - a [i].c [c]) + u.RNDprobab() * adaptiveR1 * (a [leader2Idx].c [c] - a [i].c [c]); tempPop [i].c [c] = u.SeInDiSp(tempPop [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">// PHASE class="num">2: Fluctuating market(search operators) class=class="str">"cmt">// Group class="num">2 - search class="kw">operator class="num">1 (moderate risk) for (class="type">int i = groupSize; i < class="num">2 * groupSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { class="type">class="kw">double range = rangeMax [c] - rangeMin [c]; if (u.RNDprobab() < class="num">0.5) { tempPop [i].c [c] = cB [c];class=class="str">"cmt">// tempPop [i].c [c] + delta; } else { class=class="str">"cmt">// Search around the center of the elite class="type">class="kw">double eliteCenter = class="num">0.0; for (class="type">int j = class="num">0; j < groupSize; j++) {
「第三组的高风险搜索与种群回写」
第三组(索引从 2*groupSize 到 popSize)走的是高风险搜索算子,逻辑上和精英组、第一组完全分开。每个坐标维度先取 range = rangeMax[c] - rangeMin[c],再以 riskAlpha 概率做整维重初始化,直接在区间内随机撒点,用来跳出可能的局部劣解。 若不触发重初始化,则有 0.5 + riskAlpha/2.0 的概率做广域搜索:searchRadius 被设为 2.0*riskAlpha*range*(1.0 - decayRate*0.3),再叠加上下浮动的 delta,让该维在较宽半径内游走;否则进入反向训练——取种群后半 worstCount = groupSize/2 个最差个体的维度均值 worstCenter,用 2.0*tempPop[i].c[c] - worstCenter 朝最差中心的反方向推,并补一点 ±0.1*riskAlpha*range 的噪声。 每一维算完都要过 u.SeInDiSp 做边界吸附与步长对齐,避免越界或落在不可交易粒度上。最后循环把 tempPop 中第一组以外的个体整组 ArrayCopy 回主种群 a,完成一代变异。 衰减率不是线性的:GetDecayRate 里用 progress = currentEpoch/totalEpochs 表征训练进度,非线性映射能在前中期保探索、后段促收敛。外汇与贵金属杠杆高、滑点突变频繁,这类算子实盘前务必在 MT5 策略测试器用历史 Tick 跑通边界与步长逻辑。
eliteCenter += a [j].c [c];
}
eliteCenter /= (class="type">class="kw">double)groupSize;
class="type">class="kw">double noise = riskAlpha * range * u.RNDfromCI(-class="num">0.5, class="num">0.5) * (class="num">1.0 - decayRate * class="num">0.5);
tempPop [i].c [c] = eliteCenter + noise;
}
class=class="str">"cmt">// Check boundaries
tempPop [i].c [c] = u.SeInDiSp(tempPop [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
}
}
class=class="str">"cmt">// Group class="num">3 - search class="kw">operator class="num">2 (high risk)
for (class="type">int i = class="num">2 * groupSize; i < popSize; i++)
{
for (class="type">int c = class="num">0; c < coords; c++)
{
class="type">class="kw">double range = rangeMax [c] - rangeMin [c];
if (u.RNDprobab() < riskAlpha)
{
class=class="str">"cmt">// Full reinitialization
tempPop [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
}
else
if (u.RNDprobab() < class="num">0.5 + riskAlpha / class="num">2.0)
{
class=class="str">"cmt">// Broad search
class="type">class="kw">double searchRadius = class="num">2.0 * riskAlpha * range * (class="num">1.0 - decayRate * class="num">0.3);
class="type">class="kw">double delta = u.RNDfromCI(-searchRadius, searchRadius);
tempPop [i].c [c] = tempPop [i].c [c] + delta;
}
else
{
class=class="str">"cmt">// Opposition-based training
class="type">class="kw">double worstCenter = class="num">0.0;
class="type">int worstCount = groupSize / class="num">2;
for (class="type">int j = popSize - worstCount; j < popSize; j++)
{
worstCenter += a [j].c [c];
}
worstCenter /= (class="type">class="kw">double)worstCount;
class=class="str">"cmt">// Moving in the opposite direction from the worst ones
tempPop [i].c [c] = class="num">2.0 * tempPop [i].c [c] - worstCenter;
class=class="str">"cmt">// Add a little noise
class="type">class="kw">double noise = riskAlpha * range * u.RNDfromCI(-class="num">0.1, class="num">0.1);
tempPop [i].c [c] += noise;
}
class=class="str">"cmt">// Check boundaries
tempPop [i].c [c] = u.SeInDiSp(tempPop [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
}
}
class=class="str">"cmt">// Copying from the temporary population to the main one(except group class="num">1)
for (class="type">int i = groupSize; i < popSize; i++)
{
ArrayCopy(a [i].c, tempPop [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
}
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--- Get the decay rate
class="type">class="kw">double C_AO_EMA::GetDecayRate()
{
if (totalEpochs <= class="num">0) class="kw">return class="num">0.0;
class=class="str">"cmt">// Non-linear attenuation for better exploitation/exploration balance
class="type">class="kw">double progress = (class="type">class="kw">double)currentEpoch / (class="type">class="kw">double)totalEpochs;