针对初学者以 MQL5 编写EA 交易的分步指南·进阶篇
🤖

针对初学者以 MQL5 编写EA 交易的分步指南·进阶篇

(2/3)· 接上篇概念铺垫,本篇把移动平均线与 ADX 趋势过滤写进 EA 骨架,避开新手最常卡住的向导配置

偏理论 第 2/3 篇
很多初学者一上来就写下单函数,却没先固化策略条件,结果回测和实盘逻辑对不上。先把 MA-8 与 ADX>22 的进出场规则白纸黑字定好,向导里填参数才不会乱。

◍ 用 MA 斜率和 ADX 过滤双向开仓信号

做多逻辑的第一道闸是 MA-8 的阶梯方向:柱0的 MA-8 大于柱1、柱1大于柱2,才算均线向上倾斜,这占买入四条件的一条。紧接着看柱1收盘价是否高于同周期 MA-8,两项同时满足才继续往后验,否则直接跳过剩余判断,省掉无谓计算。 ADX 当前值必须大于输入参数里的 Adx_Min(示例设 22.0),且 plusDI 需高于 minusDI,趋势强度与方向才够格。若 Buy_opened 已是 TRUE,说明持仓中,弹警示并 return,EA 等下一 tick;若为 FALSE,才用 MqlTradeRequest 填 mrequest 准备发单。 发单时 action 填 TRADE_ACTION_DEAL,买价取自 MqlTick 的 latest_price,止损 = 买价 - StopLoss 点、获利 = 买价 + TakeProfit 点,三个价格都过一遍 NormalizeDouble 对齐品种小数位。type_filling 用 ORDER_FILLING_FOK,没足够对手盘就整单不成交;OrderSend 后查 mresult 的返回码,10009 是请求成功、10008 是已挂单,命中任一即确认。 卖出是镜像版:MA-8 柱0<柱1<柱2 为向下倾斜,柱1收低于 MA-8,且 MinusDI 大于 plusDI,Sell_opened 为 FALSE 才发 ORDER_TYPE_SELL。卖价取自 latest_price 的卖价,止损获利反向加减,同样 NormalizeDouble。外汇与贵金属杠杆高,信号过滤只降概率不消风险,参数请先在 MT5 策略测试器跑验证。

MQL5 / C++
class=class="str">"cmt">//--- 输入参数
class="kw">input class="type">int      StopLoss=class="num">30;      class=class="str">"cmt">// 止损
class="kw">input class="type">int      TakeProfit=class="num">100;  class=class="str">"cmt">// 获利
class="kw">input class="type">int      ADX_Period=class="num">8;    class=class="str">"cmt">// ADX 周期
class="kw">input class="type">int      MA_Period=class="num">8;     class=class="str">"cmt">// 移动平均周期
class="kw">input class="type">int      EA_Magic=class="num">12345;  class=class="str">"cmt">// EA 幻数
class="kw">input class="type">class="kw">double   Adx_Min=class="num">22.0;    class=class="str">"cmt">// ADX 最小值
class="kw">input class="type">class="kw">double   Lot=class="num">0.1;         class=class="str">"cmt">// 交易手数
class=class="str">"cmt">//--- 其他参数
class="type">int adxHandle; class=class="str">"cmt">// 我们 ADX 指标的句柄
class="type">int maHandle;  class=class="str">"cmt">// 我们移动平均指标的句柄
class="type">class="kw">double plsDI[],minDI[],adxVal[]; class=class="str">"cmt">// 保存每个柱 +DI, -DI 和 ADX 数值的动态数组
class="type">class="kw">double maVal[]; class=class="str">"cmt">// 保存每个柱移动平均值的动态数组
class="type">class="kw">double p_close; class=class="str">"cmt">// 保存一个柱收盘价的变量
class="type">int STP, TKP;  class=class="str">"cmt">// 将用于计算止损和获利值
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 得到ADX指标句柄
   adxHandle=iADX(NULL,class="num">0,ADX_Period);
class=class="str">"cmt">//--- 得到移动平均指标的句柄
   maHandle=iMA(_Symbol,_Period,MA_Period,class="num">0,MODE_EMA,PRICE_CLOSE);
class=class="str">"cmt">//--- 如果句柄返回了无效句柄
   if(adxHandle<class="num">0 || maHandle<class="num">0)
     {
       Alert("创建指标句柄出错 - 错误: ",GetLastError(),"!!");
     }
class=class="str">"cmt">//--- 让我们用5位或3位小数的价格而不是4位小数的价格处理货币对
   STP = StopLoss;
   TKP = TakeProfit;

「用新柱触发交易前的护栏与结构初始化」

EA 在 5 位或 3 位报价品种上会把止损止盈基准放大 10 倍:当 _Digits 等于 5 或 3 时,STP 与 TKP 各乘 10,避免点值换算错位。这一步必须在订单函数外先做完,否则后续发单的止损距离会偏小一个数量级。 OnTick 里第一道闸是柱数校验:Bars(_Symbol,_Period) 小于 60 就弹 Alert 并 return。少于 60 根柱意味着均线和 ADX 缓冲区不可靠,此时硬跑策略容易在样本不足时误触发。 新柱判定靠静态变量 Old_Time 与 CopyTime 取到的 New_Time[0] 比对。复制成功且时间不等,才把 IsNewBar 置 true 并更新 Old_Time;复制失败则报 GetLastError 并 ResetLastError 后退出。非新柱直接 return,保证策略逻辑每根柱只跑一次。 通过新柱检查后,代码初始化 MqlTick / MqlTradeRequest / MqlTradeResult / MqlRates 四类结构,并用 ZeroMemory 清 mrequest。随后把 mrate、plsDI 等数组用 ArraySetAsSeries(...,true) 设为时间序列排列,确保索引 0 对应最新柱。外汇与贵金属杠杆高,样本不足或数组序列错乱都可能放大实盘风险,建议先在 MT5 策略测试器用 60 根以上历史柱验证该段。

MQL5 / C++
if(_Digits==class="num">5 || _Digits==class="num">3)
    {
      STP = STP*class="num">10;
      TKP = TKP*class="num">10;
    }
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class=class="str">"cmt">//| EA订单函数                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">void OnTick()
  {
class=class="str">"cmt">// 我们是否有足够用于操作的柱
   if(Bars(_Symbol,_Period)<class="num">60) class=class="str">"cmt">// 如果总柱数少于60
    {
      Alert("我们只有不到60个柱, EA 将要退出!!");
      class="kw">return;
    }
class=class="str">"cmt">// 我们将会使用 Old_Time 静态变量来保存柱时间.
class=class="str">"cmt">// 在每一次执行 OnTick 函数的时候我们都将检查当前柱的时间和保存的时间.
class=class="str">"cmt">// 如果柱时间不等于保存的时间,说明我们有了一个新柱.
   class="kw">static class="type">class="kw">datetime Old_Time;
   class="type">class="kw">datetime New_Time[class="num">1];
   class="type">bool IsNewBar=false;
class=class="str">"cmt">// 把最新的柱时间复制到 New_Time[class="num">0]
   class="type">int copied=CopyTime(_Symbol,_Period,class="num">0,class="num">1,New_Time);
   if(copied>class="num">0) class=class="str">"cmt">// 数据已经被成功复制
    {
      if(Old_Time!=New_Time[class="num">0]) class=class="str">"cmt">// 如果旧的时间不等于新柱的时间
        {
         IsNewBar=true;   class=class="str">"cmt">// 如果不是第一次调用,新柱已经出现
         if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("我们在此时有了新柱 ",New_Time[class="num">0]," 旧的时间是 ",Old_Time);
         Old_Time=New_Time[class="num">0];
        }
    }
   else
    {
      Alert("复制历史时间数据出错, 错误 =",GetLastError());
      ResetLastError();
      class="kw">return;
    }
class=class="str">"cmt">//--- EA 应该在有新柱的时候只检查新的交易
   if(IsNewBar==false)
    {
      class="kw">return;
    }
class=class="str">"cmt">//--- 我们是否有足够用于处理的柱
   class="type">int Mybars=Bars(_Symbol,_Period);
   if(Mybars<class="num">60) class=class="str">"cmt">// 如果总柱数少于60
    {
      Alert("我们只有不到60个柱, EA 将要退出!!");
      class="kw">return;
    }
class=class="str">"cmt">//--- 定义一些我们将用于交易的MQL5结构
   class="type">MqlTick latest_price;      class=class="str">"cmt">// 将用于取得最近/最新报价
   class="type">MqlTradeRequest mrequest; class=class="str">"cmt">// 将用于发送我们的交易请求
   class="type">MqlTradeResult mresult;   class=class="str">"cmt">// 将用于取得我们的交易结果
   class="type">MqlRates mrate[];         class=class="str">"cmt">// 将用于保存每个柱的价格,交易量和差价
   ZeroMemory(mrequest);     class=class="str">"cmt">// 初始化mrequest 结构
if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("我们在此时有了新柱 ",New_Time[class="num">0]," 旧的时间是 ",Old_Time);
class="type">int Mybars=Bars(_Symbol,_Period);
class=class="str">"cmt">/*
    让我们确认用于保存报价, ADX 值 和 MA 值的数组都以
    时间序列数组相同的形式存放
*/
class=class="str">"cmt">// 报价数组
   ArraySetAsSeries(mrate,true);
class=class="str">"cmt">// ADX DI+值数组
   ArraySetAsSeries(plsDI,true);
class=class="str">"cmt">// ADX DI-值数组

把指标缓冲区和持仓状态读进 EA

这段逻辑干的事很直接:先把 minDI、adxVal、maVal 三个数组用 ArraySetAsSeries(...,true) 设成时间序列,索引 0 就是当前值,往前推是历史。这样后面写 maVal[0]>maVal[1] 时,读到的就是「现在比上一根高」,不用自己倒腾下标。 报价和柱数据分别走 SymbolInfoTick 和 CopyRates。CopyRates(_Symbol,_Period,0,3,mrate) 一次性拉最近 3 根柱的 MqlRates 结构,mrate[1].close 就是前一根收盘价,后面拿来和 MA-8 比。任何一步返回小于 0 就 Alert 报错并 return,避免脏数据继续跑。 指标值用 CopyBuffer 按句柄搬:ADX 句柄的 0/1/2 号缓冲分别是 ADX 主线、+DI、-DI,各取 3 个值;MA 句柄 0 号缓冲是 MA-8 本身。只要其中任意一个复制失败,EA 会直接弹窗报错退出本次 tick。 持仓检查靠 PositionSelect(_Symbol) 配合 PositionGetInteger(POSITION_TYPE)。返回 POSITION_TYPE_BUY 就置 Buy_opened=true,POSITION_TYPE_SELL 则 Sell_opened=true,后面开仓模块靠这两个布尔量判断是否已占边。 买入触发条件拆成三个布尔:Buy_Condition_1 要求 MA-8 连续两根向上(maVal[0]>maVal[1] 且 maVal[1]>maVal[2]);Buy_Condition_2 要求前收 p_close 高于 MA-8(p_close>maVal[1]);Buy_Condition_3 要求当前 ADX 大于阈值 Adx_Min,代码里设的是 22。三者同时为真才倾向满足做多前置过滤。外汇和贵金属波动大,ADX>22 只代表趋势概率偏高,不代表一定延续。

MQL5 / C++
  ArraySetAsSeries(minDI,true);
class=class="str">"cmt">// ADX 值数组
  ArraySetAsSeries(adxVal,true);
class=class="str">"cmt">// MA-class="num">8 值数组
  ArraySetAsSeries(maVal,true);
class=class="str">"cmt">//--- 使用MQL5的MqlTick结构取得最后报价
  if(!SymbolInfoTick(_Symbol,latest_price))
    {
      Alert("Error getting the latest price quote - error:",GetLastError(),"!!");
      class="kw">return;
    }
class=class="str">"cmt">//--- 取得最新三个柱的详细信息
  if(CopyRates(_Symbol,_Period,class="num">0,class="num">3,mrate)<class="num">0)
    {
      Alert("复制报价/历史数据出错 - 错误:",GetLastError(),"!!");
      class="kw">return;
    }
class=class="str">"cmt">//--- 使用句柄把我们指标的新值复制到缓冲区(数组)中
  if(CopyBuffer(adxHandle,class="num">0,class="num">0,class="num">3,adxVal)<class="num">0 || CopyBuffer(adxHandle,class="num">1,class="num">0,class="num">3,plsDI)<class="num">0
      || CopyBuffer(adxHandle,class="num">2,class="num">0,class="num">3,minDI)<class="num">0)
    {
      Alert("复制 ADX 指标缓冲区出错 - 错误:",GetLastError(),"!!");
      class="kw">return;
    }
  if(CopyBuffer(maHandle,class="num">0,class="num">0,class="num">3,maVal)<class="num">0)
    {
      Alert("复制移动平均指标缓冲区出错 - 错误:",GetLastError());
      class="kw">return;
    }
class=class="str">"cmt">//--- 我们没有遇到错误,所以继续
class=class="str">"cmt">//--- 我们是否已经有持仓?
    class="type">bool Buy_opened=false;  class=class="str">"cmt">// 保存建买仓结果的变量
class="type">bool Sell_opened=false; class=class="str">"cmt">// 保存建卖仓结果的变量
    
    if (PositionSelect(_Symbol) ==true) class=class="str">"cmt">// 我们还有未平仓头寸
    {
        if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
        {
            Buy_opened = true;  class=class="str">"cmt">//是买入
        }
        else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL )
        {
            Sell_opened = true; class=class="str">"cmt">// 是卖出
        }
    }
class="type">bool  PositionSelect(
   class="type">class="kw">string  symbol       class=class="str">"cmt">// 交易品种名称
);
class="type">long  PositionGetInteger(
   ENUM_POSITION_PROPERTY  property_id   class=class="str">"cmt">// 属性标识号
   );
class=class="str">"cmt">// 复制当前柱的前一个柱的收盘价, 也就是柱 class="num">1
  p_close=mrate[class="num">1].close;  class=class="str">"cmt">// 柱1收盘价
class=class="str">"cmt">/*
   class="num">1. 检查买入设置 : MA-class="num">8 向上增加,
   前一收盘价比它高, ADX > class="num">22, +DI > -DI
*/
class=class="str">"cmt">//--- 声明bool类型变量用于保存我们的买入条件
  class="type">bool Buy_Condition_1 = (maVal[class="num">0]>maVal[class="num">1]) && (maVal[class="num">1]>maVal[class="num">2]); class=class="str">"cmt">// MA-class="num">8 向上增加
  class="type">bool Buy_Condition_2 = (p_close > maVal[class="num">1]);          class=class="str">"cmt">// 前一收盘价高于 MA-class="num">8
  class="type">bool Buy_Condition_3 = (adxVal[class="num">0]>Adx_Min);           class=class="str">"cmt">// 当前 ADX 值比最小值大 (class="num">22)

◍ 把四个条件拼成市价买入单

顺势开仓不是看一个指标就冲,这里把均线排列、价格站位、ADX 强度、DI 方向四个布尔量做了两级与判断:Buy_Condition_1 和 2 先过滤趋势结构,Buy_Condition_3 和 4 再确认动量有效。只有四者同真,才可能触发下单逻辑。 代码里最容易被忽略的是持仓去重:如果 Buy_opened 已为真,直接 Alert 并 return,避免同一信号在下一根 K 线重复发单。外汇与贵金属杠杆高,重复开仓会把风险敞口成倍放大,这一道闸门必须留着。 下单部分用 TRADE_ACTION_DEAL 走即时成交,滑点偏移设了 deviation=100 点;止损用 ask - STP*_Point、止盈用 ask + TKP*_Point,手数和幻数由外部变量 Lot、EA_Magic 控制。开 MT5 把 STP、TKP 调成你惯用的点数,就能直接验证这套请求结构是否过经纪商风控。

MQL5 / C++
class="type">bool Buy_Condition_4 = (plsDI[class="num">0]>minDI[class="num">0]);                 class=class="str">"cmt">// +DI 大于 -DI
class=class="str">"cmt">//--- 放到一起  
  if(Buy_Condition_1 && Buy_Condition_2)
   {
     if(Buy_Condition_3 && Buy_Condition_4)
      {
       class=class="str">"cmt">// 有未平买入仓位?
       if (Buy_opened)
       {
         Alert("我们已经有了买入仓位!!!");
         class="kw">return;   class=class="str">"cmt">// 不建新的买入仓位
       }
       mrequest.action = TRADE_ACTION_DEAL;                                        class=class="str">"cmt">// 立即执行订单
       mrequest.price = NormalizeDouble(latest_price.ask,_Digits);                 class=class="str">"cmt">// 最新卖家报价
       mrequest.sl = NormalizeDouble(latest_price.ask - STP*_Point,_Digits); class=class="str">"cmt">// 止损
       mrequest.tp = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits); class=class="str">"cmt">// 获利
       mrequest.symbol = _Symbol;                                                  class=class="str">"cmt">// 货币对
       mrequest.volume = Lot;                                                      class=class="str">"cmt">// 交易手数
       mrequest.magic = EA_Magic;                                                 class=class="str">"cmt">// 订单幻数
       mrequest.type = ORDER_TYPE_BUY;                                            class=class="str">"cmt">// 买入订单
       mrequest.type_filling = ORDER_FILLING_FOK;                                 class=class="str">"cmt">// 订单执行类型
       mrequest.deviation=class="num">100;                                                    class=class="str">"cmt">// 当前价格偏移
       class=class="str">"cmt">//--- 发送订单
       OrderSend(mrequest,mresult);
class="type">bool Buy_Condition_1 = (maVal[class="num">0]>maVal[class="num">1]) && (maVal[class="num">1]>maVal[class="num">2]);
class="type">bool Buy_Condition_2 = (p_close > maVal[class="num">1]);
class="type">bool Buy_Condition_3 = (adxVal[class="num">0]>Adx_Min);
class="type">bool Buy_Condition_4 = (plsDI[class="num">0]>minDI[class="num">0]);
class=class="str">"cmt">// 有买入持仓吗?
if (Buy_opened)
  {
      Alert("我们已经有了买入仓位!!!");
      class="kw">return;   class=class="str">"cmt">// 不建新的买入仓位
   }
class="type">bool  OrderSend(
  class="type">MqlTradeRequest&   request      class=class="str">"cmt">// 查询结构
  class="type">MqlTradeResult&   result        class=class="str">"cmt">// 回复结构
  );

「卖出触发与下单回执的硬判断」

买入方向的下单回执要先看 retcode。MT5 里 10009 代表请求已完成、10008 代表订单已下,这两个值任一命中即可认为票已经进场,弹窗报出订单号即可;其余情况直接抓 GetLastError() 报错误码并 ResetLastError() 后 return,避免后续逻辑在脏状态里继续跑。 卖出侧的条件拆成四个 bool 再合判。MA-8 向下要求 maVal[0]<maVal[1] 且 maVal[1]<maVal[2];前一收盘低于该均线;ADX 当前值大于 22;-DI 大于 +DI。前两条满足才进内层,再卡 ADX 与 DI 方向。 内层先查 Sell_opened 标记,已有卖出仓就 Alert 并 return,不叠仓。新建市价卖单走 TRADE_ACTION_DEAL,price 取 NormalizeDouble(latest_price.bid,_Digits),sl 挂在 bid+STP*_Point、tp 挂在 bid-TKP*_Point,symbol 用 _Symbol、volume 用 Lot 变量。外汇与贵金属杠杆高,这类条件单在滑点行情中可能拒单或滑价,参数 STP/TKP/Lot 请在策略测试器按品种点值复核。

MQL5 / C++
  class=class="str">"cmt">// 取得返回代码
  if(mresult.retcode==class="num">10009 || mresult.retcode==class="num">10008) class=class="str">"cmt">//请求完成或者已下订单
    {
      Alert("买入订单已经成功下单, 订单#:",mresult.order,"!!");
    }
  else
    {
      Alert("买入订单请求无法完成 -错误:",GetLastError());
      ResetLastError();
      class="kw">return;
    }
class=class="str">"cmt">/*
  class="num">2. 检查卖出设定 : MA-class="num">8 向下降低,
  前一收盘价低于它, ADX > class="num">22, -DI > +DI
*/
class=class="str">"cmt">//--- 声明bool类型变量用于保存我们的卖出条件
  class="type">bool Sell_Condition_1 = (maVal[class="num">0]<maVal[class="num">1]) && (maVal[class="num">1]<maVal[class="num">2]);  class=class="str">"cmt">// MA-class="num">8 向下降低
  class="type">bool Sell_Condition_2 = (p_close <maVal[class="num">1]);                         class=class="str">"cmt">// 前一收盘价低于 MA-class="num">8
  class="type">bool Sell_Condition_3 = (adxVal[class="num">0]>Adx_Min);                         class=class="str">"cmt">// 当前 ADX 值大于最小值 (class="num">22)
  class="type">bool Sell_Condition_4 = (plsDI[class="num">0]<minDI[class="num">0]);                         class=class="str">"cmt">// -DI 大于 +DI

class=class="str">"cmt">//--- 放到一起
  if(Sell_Condition_1 && Sell_Condition_2)
    {
      if(Sell_Condition_3 && Sell_Condition_4)
        {
          class=class="str">"cmt">// 还有未平卖出仓位?
          if (Sell_opened)
          {
            Alert("我们已经有了卖出仓位!!!");
            class="kw">return;  class=class="str">"cmt">// 不建新的卖出仓位
          }
          mrequest.action = TRADE_ACTION_DEAL;                                       class=class="str">"cmt">// 立即执行订单
          mrequest.price = NormalizeDouble(latest_price.bid,_Digits);                class=class="str">"cmt">// 最新买家报价
          mrequest.sl = NormalizeDouble(latest_price.bid + STP*_Point,_Digits);      class=class="str">"cmt">// 止损
          mrequest.tp = NormalizeDouble(latest_price.bid - TKP*_Point,_Digits);      class=class="str">"cmt">// 获利
          mrequest.symbol = _Symbol;                                                 class=class="str">"cmt">// 货币对
          mrequest.volume = Lot;                                                     class=class="str">"cmt">// 交易手数
把策略监控交给小布盯盘
这些 MA-8 渐增且价格收线上方、ADX 大于 22 的趋势过滤诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你专注把规则转成代码。

常见问题

通常监听 OnTick 配合比较当前柱时间与前一根柱时间,或用定时器在柱开盘后触发一次,避免同根柱内重复开仓。
可以,小布盯盘品种页内置了均线与 ADX 状态识别,能直观看到趋势过滤是否生效,省去你手动核对指标数值。
ADX 衡量趋势强度,低于阈值代表震荡概率高,顺势策略在弱趋势中容易被来回扫损,过滤掉更安全。
点数需按品种 Point 与 Digits 换算为价格差,下单时填止损价与止盈价而非直接写点数,跨品种才不会错位。