构建动态多品种EA(第三部分):均值回归与动量策略·进阶篇
(2/3)·趋势里反复吃同方向信号?这套动态框架让EA只在统计显著偏离时动手
多品种均线与波动率的句柄初始化
要在 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 启动时日志直接报品种,省得后面猜。
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); // 无信号退出;选中品种有仓且同方向则跳过,反方向则调用平仓准备反手
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,手数算错会被拒单。
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 个时略有开销,但比起下单频率可忽略。
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">//+------------------------------------------------------------------+