构建动态多品种EA(第三部分):均值回归与动量策略·进阶篇
⚙️

构建动态多品种EA(第三部分):均值回归与动量策略·进阶篇

(2/3)·趋势里反复吃同方向信号?这套动态框架让EA只在统计显著偏离时动手

实战向进阶 第 2/3 篇
很多多品种EA在单边行情里不停补同方向单,看着像顺势其实是信号疲劳堆出来的暴露。把Z分数和动量区间叠在一起,系统才分得清该加仓、该观望还是该歇着。这一篇接着把统计阈值和实时进度追踪落到代码逻辑里。

多品种均线与波动率的句柄初始化

要在 MT5 上跑跨品种策略,第一步是把品种字符串拆开并给每个品种挂上指标句柄。下面这段 OnInit 用逗号分割 Symbols 参数,统计出 Num_symbs,再把 MA_hndl、STDev_hndl、ATR_hndl 三个数组按品种数扩容,避免越界。 核心循环里对每个品种创建 H1 周期的 iMA(周期 MAPeriod、SMA、收盘价)、iStdDev(同参数)和 iATR(周期 14)。任一句柄返回 INVALID_HANDLE 就打印失败并 INIT_FAILED,这能帮你当场揪出拼写错误的品种名或经纪商不提供的数据。 MeanAndMomentum 函数展示了信号前的取数骨架:用 CopyRates 取当前 1 根 H1 K 线收盘价,再按 MomentumPeriod 偏移取历史收盘价,后续动量计算就靠这两个值差。外汇与贵金属杠杆高,句柄创建失败若不被拦截,实盘可能静默漏单。 别把初始化当走过场 很多 EA 在 OnInit 吞掉句柄错误,结果跑几天才发现某个品种根本没建指标。把 INIT_FAILED 留着,MT5 启动时日志直接报品种,省得后面猜。

MQL5 / C++
class="type">int Num_symbs = class="num">0;
class=class="str">"cmt">// Indicator handles arrays
class="type">int MA_hndl[];
class="type">int STDev_hndl[];
class="type">int ATR_hndl[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
   class=class="str">"cmt">//--- Split symbol list
   class="type">class="kw">ushort separator = StringGetCharacter(",", class="num">0);
   StringSplit(Symbols, separator, symb_List);
   Num_symbs = ArraySize(symb_List);
   class=class="str">"cmt">//--- Resize arrays
   ArrayResize(MA_hndl, Num_symbs);
   ArrayResize(STDev_hndl, Num_symbs);
   ArrayResize(ATR_hndl, Num_symbs);
   class=class="str">"cmt">//--- Prepare each symbol
   for (class="type">int i = class="num">0; i < Num_symbs; i++) {
      class="type">class="kw">string symbol = symb_List[i];
      StringTrimLeft(symbol);
      StringTrimRight(symbol);
      
      class=class="str">"cmt">//--- Create indicator handles
      MA_hndl[i] = iMA(symbol, PERIOD_H1, MAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE);
      STDev_hndl[i] = iStdDev(symbol, PERIOD_H1, MAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE);
      ATR_hndl[i] = iATR(symbol, PERIOD_H1, class="num">14);
      
      if (MA_hndl[i] == INVALID_HANDLE || STDev_hndl[i] == INVALID_HANDLE || ATR_hndl[i] == INVALID_HANDLE) {
         Print("Failed to create indicator handles for ", symbol);
         class="kw">return INIT_FAILED;
      }
   }
   
   class=class="str">"cmt">//--- Set magic number for trade identification
   trade.SetExpertMagicNumber(class="num">54321);
   
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|          Mean and Momentum Signal Generator                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void MeanAndMomentum(class="type">class="kw">string symbol, class="type">int idx) {
   class=class="str">"cmt">//--- Get current price data
   class="type">MqlRates current[];
   if(CopyRates(symbol, PERIOD_H1, class="num">0, class="num">1, current) < class="num">1) class="kw">return;
   class="type">class="kw">double close = current[class="num">0].close;
   
   class=class="str">"cmt">//--- Get historical price for momentum calculation
   class="type">MqlRates historical[];
   if(CopyRates(symbol, PERIOD_H1, MomentumPeriod, class="num">1, historical) < class="num">1) class="kw">return;
   class="type">class="kw">double histClose = historical[class="num">0].close;
   
   class=class="str">"cmt">//--- Get indicator values

「信号合成与仓位风险的代码实现」

这段逻辑把均线、标准差和 ATR 三个指标缓冲一次性拉进数组,任一拷贝失败就直接 return,避免脏数据进入后续计算。注意三个数组都只取 1 根柱的值,说明这是典型的实时Tick或新柱触发式评估,不是历史遍历。 double ma[1], stddev[1], atr[1]; if(CopyBuffer(MA_hndl[idx], 0, 0, 1, ma) < 1) return; if(CopyBuffer(STDev_hndl[idx], 0, 0, 1, stddev) < 1) return; if(CopyBuffer(ATR_hndl[idx], 0, 0, 1, atr) < 1) return; // 声明三个长度为1的 double 数组,分别接收均线、标准差、ATR 的最新值 // 从对应指标句柄拷贝 1 根柱的缓冲数据,返回值小于 1 表示拷贝失败,直接退出函数 动量用 close - histClose 表达,zscore 则是价格偏离均值的标准差倍数,分母做了 >0 保护防止除零。动态动量阈值 momThreshold 直接等于 Mom_Threshold 乘标准差,市场波动大时阈值自动抬高。 double momentum = close - histClose; double zscore = (stddev[0] > 0) ? (close - ma[0]) / stddev[0] : 0; double momThreshold = Mom_Threshold * stddev[0]; // momentum:当前收盘价减历史收盘价,衡量价格变动 // zscore:价格减均线再除标准差,得到标准化偏离度,标准差为0时记0 // momThreshold:用参数乘标准差,得到随波动缩放的动量门槛 信号判定分两类:均值回归看 zscore 是否突破 Z_Threshold(正负对称),动量看 momentum 是否过动态阈值。优先级写死为动量 > 均值回归,两者同向共振时给 High 风险配比,单纯动量给 Mod,单纯回归给 Low。 bool meanReversionLong = (zscore < -Z_Threshold); bool meanReversionShort = (zscore > Z_Threshold); bool momentumLong = (momentum > momThreshold); bool momentumShort = (momentum < -momThreshold); // 四个布尔分别标记:向下偏离做多、向上偏离做空、动量向上、动量向下 int signal = 0; double riskPercent = 0; if(momentumLong && meanReversionLong) { signal = 1; riskPercent = RiskPercent_High; } else if(momentumShort && meanReversionShort) { signal = -1; riskPercent = RiskPercent_High; } else if(momentumLong) { signal = 1; riskPercent = RiskPercent_Mod; } else if(momentumShort) { signal = -1; riskPercent = RiskPercent_Mod; } else if(meanReversionLong) { signal = 1; riskPercent = RiskPercent_Low; } else if(meanReversionShort) { signal = -1; riskPercent = RiskPercent_Low; } // signal=1 为多,-1 为空,0 无信号;riskPercent 按共振/纯动量/纯回归三档赋值 无信号直接 return;若同品种已有持仓且方向一致则不加仓,方向相反先平再开。外汇与贵金属杠杆高,这套动态阈值在跳空行情中可能连续触发,实盘前请在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 跑一遍验证信号频率。 if(signal == 0) return; if(PositionSelect(symbol)) { long positionType = PositionGetInteger(POSITION_TYPE);

if((positionType == POSITION_TYPE_BUY && signal == 1)

(positionType == POSITION_TYPE_SELL && signal == -1)) { return; } else { trade.PositionClose(symbol); // 无信号退出;选中品种有仓且同方向则跳过,反方向则调用平仓准备反手

MQL5 / C++
class="type">class="kw">double ma[class="num">1], stddev[class="num">1], atr[class="num">1];
if(CopyBuffer(MA_hndl[idx], class="num">0, class="num">0, class="num">1, ma) < class="num">1) class="kw">return;
if(CopyBuffer(STDev_hndl[idx], class="num">0, class="num">0, class="num">1, stddev) < class="num">1) class="kw">return;
if(CopyBuffer(ATR_hndl[idx], class="num">0, class="num">0, class="num">1, atr) < class="num">1) class="kw">return;

class=class="str">"cmt">//--- Calculate metrics
class="type">class="kw">double momentum = close - histClose;
class="type">class="kw">double zscore = (stddev[class="num">0] > class="num">0) ? (close - ma[class="num">0]) / stddev[class="num">0] : class="num">0;
class="type">class="kw">double momThreshold = Mom_Threshold * stddev[class="num">0]; class=class="str">"cmt">// Dynamic momentum threshold

class=class="str">"cmt">//--- Determine signal type
class="type">int signal = class="num">0;
class="type">class="kw">double riskPercent = class="num">0;

class="type">bool meanReversionLong = (zscore < -Z_Threshold);
class="type">bool meanReversionShort = (zscore > Z_Threshold);
class="type">bool momentumLong = (momentum > momThreshold);
class="type">bool momentumShort = (momentum < -momThreshold);

class=class="str">"cmt">//--- Signal priority: Momentum > Mean Reversion
if(momentumLong && meanReversionLong) {
  signal = class="num">1;
  riskPercent = RiskPercent_High; class=class="str">"cmt">// Strong signal
}
else if(momentumShort && meanReversionShort) {
  signal = -class="num">1;
  riskPercent = RiskPercent_High;
}
else if(momentumLong) {
  signal = class="num">1;
  riskPercent = RiskPercent_Mod;
}
else if(momentumShort) {
  signal = -class="num">1;
  riskPercent = RiskPercent_Mod;
}
else if(meanReversionLong) {
  signal = class="num">1;
  riskPercent = RiskPercent_Low;
}
else if(meanReversionShort) {
  signal = -class="num">1;
  riskPercent = RiskPercent_Low;
}

class=class="str">"cmt">//--- Exit if no signal
if(signal == class="num">0) class="kw">return;

class=class="str">"cmt">//--- Check existing positions
if(PositionSelect(symbol)) {
  class="type">long positionType = PositionGetInteger(POSITION_TYPE);
  if((positionType == POSITION_TYPE_BUY && signal == class="num">1) ||
     (positionType == POSITION_TYPE_SELL && signal == -class="num">1)) {
    class="kw">return; class=class="str">"cmt">// Already in position in same direction
  }
  else {
    class=class="str">"cmt">// Close opposite position before opening new one
    trade.PositionClose(symbol);

◍ 按波动率和账户风险反推手数

风险仓位不能拍脑袋定。下面这段逻辑用账户余额和 ATR 波动共同约束单笔亏损,再换算成可下手的 lot 数,外汇与贵金属杠杆高,实际回撤可能远超直观预期。 核心函数是 CalculatePositionSize:先取 AccountInfoDouble(ACCOUNT_BALANCE) 得到余额,乘以 riskPercent/100 算出本次允许亏的本金。止损距离直接取 atrValue * 1.5,也就是 1.5 倍当前 ATR,属于偏紧的波动止损。 把止损距离乘以每跳价值比 (tickValue/tickSize) 得到每标准手亏损额 lossPerLot,再用 riskAmount 除以它得到原始 lots。若 broker 的 point、tickValue、tickSize 任一非正,函数直接返回 0 不下单,避免脏参数成交。 NormalizeLots 负责把 lots 对齐到券商最小体积和步进:MathRound(lots/lotStep)*lotStep 做四舍五入,再用 MathMax(minLot, MathMin(maxLot, lots)) 夹在 [SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_MAX] 内。开 MT5 把 SYMBOL_VOLUME_STEP 打印出来,常会看到 0.01 或 0.1,手数算错会被拒单。

MQL5 / C++
class="type">class="kw">double CalculatePositionSize(class="type">class="kw">string symbol, class="type">class="kw">double riskPercent, class="type">class="kw">double atrValue) {
   class="type">class="kw">double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   class="type">class="kw">double riskAmount = balance * (riskPercent / class="num">100.0);
   
   class="type">class="kw">double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   class="type">class="kw">double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE_LOSS);
   class="type">class="kw">double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   
   if(point <= class="num">0 || tickValue <= class="num">0 || tickSize <= class="num">0) {
      Print("Invalid symbol parameters for ", symbol);
      class="kw">return class="num">0;
   }
   
   class=class="str">"cmt">// Use ATR-based stop loss
   class="type">class="kw">double slDistance = atrValue * class="num">1.5;
   class="type">class="kw">double lossPerLot = slDistance * (tickValue / tickSize);
   
   if(lossPerLot <= class="num">0) {
      Print("Invalid loss calculation for ", symbol);
      class="kw">return class="num">0;
   }
   
   class="type">class="kw">double lots = riskAmount / lossPerLot;
   lots = NormalizeLots(symbol, lots);
   
   class="kw">return lots;
}

class="type">class="kw">double NormalizeLots(class="type">class="kw">string symbol, class="type">class="kw">double lots) {
   class="type">class="kw">double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
   class="type">class="kw">double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
   class="type">class="kw">double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
   
   if(lotStep > class="num">0) {
      lots = MathRound(lots / lotStep) * lotStep;
   }
   
   lots = MathMax(minLot, MathMin(maxLot, lots));
   class="kw">return lots;
}

用 ATR 动态挂单的执行函数

均值回归叠加动量系统里,真正下单的动作都收在 ExecuteTrade 里。它先抓品种 point 和当前 ask/bid,再尝试从 ATR 句柄拷最新一根值,用来算止损止盈距离。 ATR 正常时,止损取 atr[0]*1.5、止盈取 atr[0]*2.5,买价上方挂 tp、下方挂 sl,卖单反向。这样仓位的风险回报比固定在 1:1.67 附近,随波动率自动伸缩。 若 CopyBuffer 失败或品种不在监听数组,就退回写死的 StopLoss、TakeProfit(以 point 倍数计)。外汇与贵金属杠杆高,ATR 突变时滑点可能吃掉预设距离,实盘前务必在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍确认。 ArrayPosition 只是线性扫 symb_List 返回下标,符号超 50 个时略有开销,但比起下单频率可忽略。

MQL5 / C++
class="type">void ExecuteTrade(ENUM_ORDER_TYPE tradeType, class="type">class="kw">string symbol, class="type">class="kw">double lotSize) {
   class="type">class="kw">double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   class="type">class="kw">double price = (tradeType == ORDER_TYPE_BUY) ?
                       SymbolInfoDouble(symbol, SYMBOL_ASK) :
                       SymbolInfoDouble(symbol, SYMBOL_BID);
   
   class=class="str">"cmt">// Get current ATR for dynamic stop levels
   class="type">class="kw">double atr[class="num">1];
   class="type">int idx = ArrayPosition(symbol);
   if(idx >= class="num">0 && CopyBuffer(ATR_hndl[idx], class="num">0, class="num">0, class="num">1, atr) > class="num">0) {
      class="type">class="kw">double slDistance = atr[class="num">0] * class="num">1.5;
      class="type">class="kw">double tpDistance = atr[class="num">0] * class="num">2.5;
      
      class="type">class="kw">double sl = (tradeType == ORDER_TYPE_BUY) ?
                        price - slDistance :
                        price + slDistance;
                        
      class="type">class="kw">double tp = (tradeType == ORDER_TYPE_BUY) ?
                        price + tpDistance :
                        price - tpDistance;
      
      trade.PositionOpen(symbol, tradeType, lotSize, price, sl, tp, "MR-Mom System");
   }
   else {
      class=class="str">"cmt">// Fallback to fixed stops if ATR fails
      class="type">class="kw">double sl = (tradeType == ORDER_TYPE_BUY) ?
                        price - (StopLoss * point) :
                        price + (StopLoss * point);
                        
      class="type">class="kw">double tp = (tradeType == ORDER_TYPE_BUY) ?
                        price + (TakeProfit * point) :
                        price - (TakeProfit * point);
      
      trade.PositionOpen(symbol, tradeType, lotSize, price, sl, tp, "MR-Mom System");
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find symbol position in array                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int ArrayPosition(class="type">class="kw">string symbol) {
   for(class="type">int i = class="num">0; i < Num_symbs; i++) {
      if(symb_List[i] == symbol) class="kw">return i;
   }
   class="kw">return -class="num">1;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
交给小布盯盘看盘口
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到Z分数偏离带和动量区间提示,你只管判断要不要手动干预。

常见问题

黄金和白银的波动率通常高于直盘货币,±2.0可能频繁触发,实战中可先按品种波动率分层测试±1.5到±2.5区间,外汇贵金属均属高风险,需样本外验证。
常用做法是追踪动量区间阈值与最近N根K线的斜率变化,当价格仍创新高但斜率走平或转负,倾向视为衰竭,概率上更宜减仓而非反向。
目前小布的品种页提供偏离带与动量区可视化,完整EA执行仍需自行部署代码,但诊断与预警部分已覆盖本篇核心阈值。
可在EA里维护相关性矩阵,对同簇品种按簇上限控仓,否则EURUSD与GBPUSD同开会隐性放大风险,市场突变时亏损可能同步。