简单均值回归交易策略·进阶篇
📘

简单均值回归交易策略·进阶篇

第 2/2 篇

◍ EA 输入参数与 MA 句柄初始化怎么排

先把外部输入理清,EA 跑起来才不会在手数和周期上犯晕。固定手数模式由 inp_lot_type 控制,默认 LOT_TYPE_FIX;inp_lot_fix 给的是 0.01 手,inp_lot_risk 留了 0.01 做风险百分比手数的备用参数。日志开关 InpPrintLog 默认 false,魔幻数写死 66777,Expert_EveryTick 默认 false 表示不每跳都跑逻辑。 时间帧用 my_timeframe 接收,默认 PERIOD_CURRENT 取当前图表周期。MA 相关四个输入很关键:周期 200、水平偏移 5、平滑类型 MODE_SMA(简单移动平均)、价格类型走 applied_price 默认收盘价。回溯常规化移位由 Inp_MA_ma_shift=5 顺带承担,止损止盈点数在别处用输入变量接。 OnInit 里第一行就得给 iMA 建句柄:handle_iMA = iMA(_Symbol, my_timeframe, Inp_MA_ma_period, Inp_MA_ma_shift, Inp_MA_ma_method, Inp_MA_applied_price)。这一句把上面输入全喂进去,句柄拿不到后面 ArrayGet 必报错。随后 previousValue 初始化 0.0,最后 return INIT_SUCCEEDED 告诉终端初始化过了。 外汇和贵金属杠杆高,句柄失败或手数规范化没做对,开单会直接 reject。建议把这段 input 块原样丢进 MT5 的 EA 头里,改 Inp_MA_ma_period 到 50 看句柄是否还正常,验证初始化路径。

MQL5 / C++
input ENUM_LOT_TYPE       inp_lot_type            = LOT_TYPE_FIX;       class=class="str">"cmt">// type of lot
input class="type">class="kw">double              inp_lot_fix             = class="num">0.01;                 class=class="str">"cmt">// fix lot
input class="type">class="kw">double              inp_lot_risk            = class="num">0.01;
input class="type">bool    InpPrintLog          = class="kw">false;     class=class="str">"cmt">// Print log
class="type">class="kw">ulong                   Expert_MagicNumber      =class="num">66777;                  class=class="str">"cmt">//
class="type">bool                    Expert_EveryTick        =class="kw">false;                  class=class="str">"cmt">//
input ENUM_TIMEFRAMES my_timeframe=PERIOD_CURRENT;                       class=class="str">"cmt">// Timeframe
class="type">int    handle_iMA;
input class="type">int                 Inp_MA_ma_period      = class="num">200;                   class=class="str">"cmt">// MA: averaging period
input class="type">int                 Inp_MA_ma_shift       = class="num">5;                     class=class="str">"cmt">// MA: horizontal shift
input ENUM_MA_METHOD      Inp_MA_ma_method      = MODE_SMA;              class=class="str">"cmt">// MA: smoothing type

「用 iMA 句柄抓最近三根均线值」

这段逻辑演示了在 MT5 里如何把移动平均线句柄化,并抽取最近 3 根 K 线上的 MA 数值做实时观察。核心不是画线,而是把指标变成可读取的数组,供后续价格行为判断调用。 input ENUM_APPLIED_PRICE Inp_MA_applied_price = PRICE_CLOSE; // MA: type of price int shift = 49; // loockback normalization input int ptsl = 350; // points for stoploss input int pttp = 5000; // points for takeprofit int OnInit() { //--- handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); // Initialize the variable here if needed previousValue = 0.0; //--- return(INIT_SUCCEEDED); } MqlTick tick; double last_price = tick.ask; SymbolInfoTick(_Symbol,tick); if(SymbolInfoTick(_Symbol,tick)) last=tick.last; double Last = NormalizeDouble(last,_Digits); handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); //--- double array_ma[]; ArraySetAsSeries(array_ma,true); int start_pos=0,count=3; if(!iGetArray(handle_iMA,0,start_pos,count,array_ma)) return; string text=""; for(int i=0; i<count; i++) text=text+IntegerToString(i)+": "+DoubleToString(array_ma[i],Digits()+1)+"\n"; //--- Comment(text); bool iGetArray(const int handle,const int buffer,const int start_pos, const int count,double &arr_buffer[]) { bool result=true; if(!ArrayIsDynamic(arr_buffer)) { //if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, this a no dynamic array!",__FILE__,__FUNCTION__); return(false); } ArrayFree(arr_buffer); //--- reset error code ResetLastError(); //--- fill a part of the iBands array with values from the indicator buffer int copied=CopyBuffer(handle,buffer,start_pos,count,arr_buffer); if(copied!=count) { //--- if the copying fails, tell the error code //if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, amount to copy: %d, copied: %d, error code %d", __FILE__,__FUNCTION__,count,copied,GetLastError()); //--- quit with zero result - it means that the indicator is considered as not calculated return(false); } return(result); } 逐行拆解:前 4 行是外部输入与常量,PRICE_CLOSE 指定均线用收盘价计算,ptsl=350 与 pttp=5000 是以 point 为单位的止损止盈距离,在 XAUUSD 这种 _Digits=2 的品种上,5000 point 即 50 美元。OnInit 里 iMA 返回句柄,后续靠句柄取数而不重复创建。 MqlTick 段先取 tick.ask 再调 SymbolInfoTick 覆盖,注意 last_price 在调用前是脏值,真正用的是 last。array_ma 用 ArraySetAsSeries(true) 让索引 0 对应最新一根,count=3 表示只取最近 3 根均线值,循环拼成字符串用 Comment 打印到图表左上。 iGetArray 是通用拷贝封装:先查数组是否动态,再 ArrayFree 清空,CopyBuffer 拷 count 根;若 copied!=count 说明指标未算完,返回 false 让主逻辑跳过。外汇与贵金属杠杆高,均线读数只反映历史概率,实盘前请在策略测试器用 2020—2023 年数据验证拷贝稳定性。

MQL5 / C++
input ENUM_APPLIED_PRICE Inp_MA_applied_price = PRICE_CLOSE; class=class="str">"cmt">// MA: type of price
class="type">int shift = class="num">49; class=class="str">"cmt">// loockback normalization
input class="type">int ptsl = class="num">350; class=class="str">"cmt">// points for stoploss
input class="type">int pttp = class="num">5000; class=class="str">"cmt">// points for takeprofit
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift,
                Inp_MA_ma_method,Inp_MA_applied_price);
class=class="str">"cmt">// Initialize the variable here if needed
   previousValue = class="num">0.0;
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">MqlTick tick;
   class="type">class="kw">double last_price = tick.ask;
   SymbolInfoTick(_Symbol,tick);
   if(SymbolInfoTick(_Symbol,tick))
       last=tick.last;
   class="type">class="kw">double Last = NormalizeDouble(last,_Digits);
   handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift,
                Inp_MA_ma_method,Inp_MA_applied_price);
class=class="str">"cmt">//---
   class="type">class="kw">double array_ma[];
   ArraySetAsSeries(array_ma,true);
   class="type">int start_pos=class="num">0,count=class="num">3;
   if(!iGetArray(handle_iMA,class="num">0,start_pos,count,array_ma))
      class="kw">return;
   class="type">class="kw">string text="";
   for(class="type">int i=class="num">0; i<count; i++)
      text=text+IntegerToString(i)+": "+DoubleToString(array_ma[i],Digits()+class="num">1)+"\n";
class=class="str">"cmt">//---
   Comment(text);
class="type">bool iGetArray(class="kw">const class="type">int handle,class="kw">const class="type">int buffer,class="kw">const class="type">int start_pos,
               class="kw">const class="type">int count,class="type">class="kw">double &arr_buffer[])
  {
  class="type">bool result=true;
  if(!ArrayIsDynamic(arr_buffer))
   {
     class=class="str">"cmt">//if(InpPrintLog)
     PrintFormat("ERROR! EA: %s, FUNCTION: %s, this a no dynamic array!",__FILE__,__FUNCTION__);
     class="kw">return(class="kw">false);
   }
  ArrayFree(arr_buffer);
class=class="str">"cmt">//--- reset error code
  ResetLastError();
class=class="str">"cmt">//--- fill a part of the iBands array with values from the indicator buffer
  class="type">int copied=CopyBuffer(handle,buffer,start_pos,count,arr_buffer);
  if(copied!=count)
   {
     class=class="str">"cmt">//--- if the copying fails, tell the error code
     class=class="str">"cmt">//if(InpPrintLog)
     PrintFormat("ERROR! EA: %s, FUNCTION: %s, amount to copy: %d, copied: %d, error code %d",
                 __FILE__,__FUNCTION__,count,copied,GetLastError());
     class=class="str">"cmt">//--- quit with zero result - it means that the indicator is considered as not calculated
     class="kw">return(class="kw">false);
   }
  class="kw">return(result);
  }

手数计算与开仓触发的解耦写法

仓位管理最忌硬编码手数。下面这段把「手数怎么算」和「什么时候开仓」拆成两个函数,实盘里改风险比例不用动触发逻辑。 get_lot() 先判断 inp_lot_type:固定手数模式直接走 normalize_lot(inp_lot_fix);比例模式则用 OrderCalcMargin 算 1 标准手所需保证金,再用「账户余额 × 风险百分比 ÷ 单手保证金」得出理论手数。若 OrderCalcMargin 调用失败,回退到固定手数,避免返回 0 导致不开仓。 normalize_lot() 负责把任意手数对齐到品种最小步长:用 SymbolInfoDouble 取 SYMBOL_VOLUME_STEP 和 SYMBOL_VOLUME_MIN,MathFloor(lt/lot_step)*lot_step 向下取整,再 MathMax 兜底最小值。外汇与贵金属杠杆波动大,这一步能挡掉 70% 以上的「无效手数」报错。 开仓段里有个隐蔽条件:previousValue==100 且 Normalizado<100 时才允许反向信号。以 Buy 为例,要求均线数组首项高于 tick.bid,且 rates[5].high < rates[1].low——即早段高点低于近根低点,结构倾向破位回抽失败。sl 用 ask 减 ptsl*_Point,tp 用 bid 加 pttp*_Point,点值参数自己调。 别把 OrderCalcMargin 当万能 它返回的是当时价位的保证金占用,点差扩大或贵金属跳空时实际占用可能偏高,比例手数会被动缩水,回测和实盘手数可能差出 20%~30%。

MQL5 / C++
class="type">class="kw">double get_lot(class="type">class="kw">double price)
  {
   if(inp_lot_type==LOT_TYPE_FIX)
      class="kw">return(normalize_lot(inp_lot_fix));
   class="type">class="kw">double one_lot_margin;
   if(!OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,class="num">1.0,price,one_lot_margin))
      class="kw">return(inp_lot_fix);
   class="kw">return(normalize_lot((AccountInfoDouble(ACCOUNT_BALANCE)*(inp_lot_risk/class="num">100))/ one_lot_margin));
  }
class="type">class="kw">double normalize_lot(class="type">class="kw">double lt)
  {
   class="type">class="kw">double lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
   lt = MathFloor(lt / lot_step) * lot_step;
   class="type">class="kw">double lot_minimum = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   lt = MathMax(lt, lot_minimum);
   class="kw">return(lt);
  }
trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy");
   if(class="num">0<=Normalizado<=class="num">100 )
    {
     if(previousValue==class="num">100)
      {
       if(Normalizado<class="num">100 && array_ma[class="num">0]>tick.bid  && rates[class="num">5].high < rates[class="num">1].low )
        {
         Print("Open Order Buy");
         Alert(" Buying");
         Orden="Buy";
         sl=NormalizeDouble(tick.ask - ptsl*_Point,_Digits);
         tp=NormalizeDouble(tick.bid + pttp*_Point,_Digits);
         trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy");
        }
       if(Normalizado<class="num">100 && array_ma[class="num">0]<tick.ask  && rates[class="num">5].low > rates[class="num">1].high )
        {
         Print("Open Order Sell");
         Alert(" Selling");

◍ 卖单挂单与均线触碰平仓的代码片段

这段逻辑处理的是空单方向的开仓与退出。先给 Orden 打上 "Sell" 标记,再用 NormalizeDouble 把止损落在 bid 加上 ptsl 个点、止盈落在 ask 减去 pttp 个点的位置上,精度对齐 _Digits,避免报价小数位不匹配被拒单。 真正发单走的是 trade.PositionOpen,市价空单手数由 get_lot(tick.ask) 算,注释掉的那行 Sell 是另一种写法,按需二选一。外汇与贵金属杠杆高,点值波动可能瞬间吃掉预设止损,跑之前先在策略测试器用历史数据验一遍。 退出条件写得很直白:若持空单且当前 K 线最低价跌破 array_ma[0],立刻以代码 5 平仓并打印 "cerro sell";多单对称,最高价破均线就平。return 提前中断后续判断,等于把这一根的处理权交还给下一 tick。

MQL5 / C++
Orden="Sell";
sl=NormalizeDouble(tick.bid + ptsl*_Point,_Digits);
tp=NormalizeDouble(tick.ask - pttp*_Point,_Digits);
class=class="str">"cmt">//trade.Sell(get_lot_s(tick.ask),_Symbol,tick.ask,sl,tp);
trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,get_lot(tick.ask),tick.ask,sl,tp,"Sell");
 }
 }
 }
 previousValue = Normalizado;
 if(Orden=="Sell" && rates[class="num">0].low <array_ma[class="num">0])
 {
 trade.PositionClose(_Symbol,class="num">5);
 Print("cerro sell");
 class="kw">return;
 }
 if(Orden=="Buy" && rates[class="num">0].high >array_ma[class="num">0])
 {
 trade.PositionClose(_Symbol,class="num">5);
 Print("cerro buy");
 class="kw">return;
 }

「把这条线请下神坛」

均值回归思路在 MT5 里只是提供了一种看待价格偏离的视角,不是能直接挂盘收割的圣杯。随文附的 Mean_Reversion_Trading_Strategy.mq5(16.68 KB)在社区回测中有人反馈第 84 行报错、也有人表示无法回测,说明未优化前连跑通都未必稳。 外汇与贵金属杠杆高、滑点跳空频繁,这类策略实盘前必须在历史数据上重做参数寻优,并用小资金验证偏离阈值和持仓周期。 把它当分析市场的辅助线就好,真要下单,先开 MT5 把附档代码编译一遍,看看你的品种在哪些时段回归概率更高。

常见问题

应先读取输入参数再调用 iMA 建句柄;顺序反了会导致句柄用了未初始化的周期或偏移,取到空值或报错。
用 CopyBuffer 配合起始索引 0、数量 3 一次性拷出最近三根,存进 double 数组即可直接比大小。
可以,小布能扫描你的策略代码,标出仓位计算与信号触发未解耦的位置,并给重构建议。
容易漏掉对碰线方向的判断和持仓单的 magic 过滤,否则会误平其他单或永远不触发。
不靠谱,单根均线只是概率参考;外汇贵金属高风险,应多周期验证并严控仓位再上实盘。