MQL5 简介:如何编写简单的EA 交易和自定义指标·进阶篇
🛠️

MQL5 简介:如何编写简单的EA 交易和自定义指标·进阶篇

(2/3)· 接基础概念,本篇手把手把向导生成的空壳填成能加载的自动交易与图表指标

实战向进阶 第 2/3 篇
不少交易者以为 MQL5 向导生成的就是完整策略,其实那只是带参数的空壳。直接编译加载后什么都不做,反而让人误以为自动交易很简单。本篇从填代码这一步开始,把空模板变成真能跑的程序。

用 ATR 偏移画通道的算价循环

这段自定义指标的核心在 OnCalculate:先通过 CopyBuffer 把 ATR 指标的 0 号缓冲前 2 根取到本地 atr 数组,并用 ArraySetAsSeries(atr,true) 把序列倒序,使 atr[1] 代表上一根 K 线的 ATR 值。 循环从 prev_calculated 起步,只对新增柱线算量。day_t 由 time[i] 除以 PeriodSeconds(ATRtimeframe) 得到,本质是按所选周期(如日线)给每根报价柱打“第几个周期”的整型标签。 当 day_n 小于 day_t,说明跨入了新周期,重置 h_day / l_day 为当前柱高低;否则就在原周期内持续追踪最高与最低。bu[i] 和 bd[i] 分别等于周期内最低价加 atr[1]、最高价减 atr[1],形成动态通道。 外汇与贵金属波动受杠杆放大,通道仅描述历史波动边界,实盘突破倾向不代表趋势确认,需结合其他信号。开 MT5 把 ATRtimeframe 设成 PERIOD_D1、ATRper 设 14,即可看日级通道对外汇品种的反应。

MQL5 / C++
  SetIndexBuffer(class="num">1,bd,INDICATOR_DATA);
  hATR=iATR(NULL,ATRtimeframe,ATRper);
}
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime& time[],
                const class="type">class="kw">double& open[],
                const class="type">class="kw">double& high[],
                const class="type">class="kw">double& low[],
                const class="type">class="kw">double& close[],
                const class="type">long& tick_volume[],
                const class="type">long& volume[],
                const class="type">int& spread[])
  {
   class="type">int i,day_n,day_t;
   class="type">class="kw">double atr[],h_day,l_day;
   CopyBuffer(hATR,class="num">0,class="num">0,class="num">2,atr);
   ArraySetAsSeries(atr,true);
   for(i=prev_calculated;i<rates_total;i++)
     {
      day_t=time[i]/PeriodSeconds(ATRtimeframe);
      if(day_n<day_t)
        {
         day_n=day_t;
         h_day=high[i];
         l_day=low[i];
        }
       else
        {
         if(high[i]>h_day) h_day=high[i];
         if(low[i]<l_day) l_day=low[i];
        }
      bu[i]=l_day+atr[class="num">1];
      bd[i]=h_day-atr[class="num">1];
     }
   class="kw">return(rates_total);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
   IndicatorRelease(hATR);
  }
    day_t=time[i]/PeriodSeconds(ATRtimeframe);

「把交易逻辑落进 EA 的骨头里」

这一节直接动手写只跑单一品种的 EA。多 EA 同品种叠加要先算清总持仓冲击,不在本篇范围内,所以默认你只挂这一个实例。 输入参数先定死几个能现场调的:StartHour 和 EndHour 框住挂单时段;MAper 给简单移动平均的周期,专门用来做追踪止损的基准;Lots 就是每单手数。 OnInit 里用 iMA 抓 MA 句柄、用 iCustom 抓上节写的自定义指标——iCustom 第三个参数填文件名(不含扩展名),路径相对 MQL5\Indicators\。 OnTick 是报价驱动的入口。先开 t[]、h[]、l[] 三个数组收当日每根 K 的时间、最高、最低,复制量用 i=(dt.hour+1)*60 估算,CopyTime/CopyHigh/CopyLow 任一个返回 -1 就 Print('Can't copy timeseries!') 并 return。数组一律 ArraySetAsSeries 设成时序索引,从新到旧。 挂单价这样算:买停损在当日最高 lev_h 上加 Spread+_Point;卖停损从当日最低 lev_l 减一个 _Point。自定义指标上轨 atr_h[] 和下轨 atr_l[] 用 CopyBuffer 取,atr_l[0] 要补回点差才是图表卖价可用的获利线。 持仓修改看 PositionsTotal 循环:买单止损取 max(MA值, lev_l),卖单对称;新值必须离开当前买/卖价至少 STOP_LEVEL*_Point,否则 OrderSend 会被拒。挂单这边用 OrdersTotal 扫,买停损若超 atr_h[] 或不在时段内直接删,价格不够高就 TRADE_ACTION_MODIFY 推上去。 没单的时候在时段内补 TRADE_ACTION_PENDING:bord/sord 标志防重挂,价格卡在 atr_l[]~atr_h[] 区间内才放单。OnDeinit 里 IndicatorRelease 释放指标资源,编译过就能拖图表上跑。外汇和贵金属杠杆高,挂单被滑点拒单是常态,上 MT5 先用策略测试器跑一周再上实盘。

MQL5 / C++
class="macro">#class="kw">property copyright  "class="num">2010, MetaQuotes Software Corp."
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property description "这个EA交易程序在每天的StartHour和EndHour"
class="macro">#class="kw">property description "之间挂单.每个订单的止损价格是价格区间的

◍ 日内高低与均线夹击的开仓触发

这段逻辑把交易窗口锁在每天 7 点到 19 点(StartHour=7、EndHour=19),用 240 周期 SMA(MAper=240)和自定义指标 indicator_TP 共同决定挂单位置。订单成交后,获利单的止损会被推到 SMA 数值上,止盈则取 indicator_TP 算出的水平——只针对已盈利持仓,亏损单不动。 代码先在 OnInit 里拿到两个句柄:hMA 是收盘价 SMA,hCI 是外部 TP 指标。OnTick 开头先拉时间、高低、均线三类序列,若 CopyTime/CopyHigh/CopyLow 任意一项返回条数不足就直接 Print 报错退出,避免拿半截数据瞎算。 日内极值不是只看最新一根 K。循环从 i=1 扫到当天零点边界,只要还在同一自然日(用 MathFloor(t[i]/86400) 判日),就持续刷新 lev_h 和 lev_l。最后 lev_h 加 Spread 和点差缓冲,lev_l 减一个点,作为突破挂单的触发线。外汇与贵金属杠杆高,这类突破单在消息时段可能滑点放大,实盘前先用策略测试器跑一轮。 别把 240 周期 SMA 当万能轴线 不同品种波动结构差很多,XAUUSD 在 240 SMA 上的回撤深度常比 EURUSD 深 30%~50%,直接套同参数可能频繁被扫。开 MT5 把 MAper 调到 120 对比一下成交密度再说。

MQL5 / C++
class="macro">#class="kw">property description "相反方向的值."当订单被执行后, 获利值"
class="macro">#class="kw">property description "被设定为 &class="macro">#x27;indicator_TP&class="macro">#x27; 计算的水平. 止损移动到"
class="macro">#class="kw">property description "SMA 的值,只针对获利订单."
input class="type">int    StartHour = class="num">7;
input class="type">int    EndHour   = class="num">19;
input class="type">int    MAper     = class="num">240;
input class="type">class="kw">double Lots       = class="num">0.1;
class="type">int hMA,hCI;
class="type">void OnInit()
  {
   hMA=iMA(NULL,class="num">0,MAper,class="num">0,MODE_SMA,PRICE_CLOSE);
   hCI=iCustom(NULL,class="num">0,"indicator_TP");
  }
class="type">void OnTick()
  {
   class="type">MqlTradeRequest request;
   class="type">MqlTradeResult result;
   class="type">MqlDateTime dt;
   class="type">bool bord=false, sord=false;
   class="type">int i;
   class="type">ulong ticket;
   class="type">class="kw">datetime t[];
   class="type">class="kw">double h[], l[], ma[], atr_h[], atr_l[],
         lev_h, lev_l, StopLoss,
         StopLevel=_Point*SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL),
         Spread   =NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK) - SymbolInfoDouble(Symbol(),SYMBOL_BID),_Digits);
   request.symbol       =Symbol();
   request.volume       =Lots;
   request.tp           =class="num">0;
   request.deviation    =class="num">0;
   request.type_filling=ORDER_FILLING_FOK;
   TimeCurrent(dt);
   i=(dt.hour+class="num">1)*class="num">60;
if(CopyTime(Symbol(),class="num">0,class="num">0,i,t)<i || CopyHigh(Symbol(),class="num">0,class="num">0,i,h)<i || CopyLow(Symbol(),class="num">0,class="num">0,i,l)<i)
     {
       Print("Can&class="macro">#x27;t copy timeseries!");
       class="kw">return;
     }
   ArraySetAsSeries(t,true);
   ArraySetAsSeries(h,true);
   ArraySetAsSeries(l,true);
   lev_h=h[class="num">0];
   lev_l=l[class="num">0];
   for(i=class="num">1;i<ArraySize(t) && MathFloor(t[i]/class="num">86400)==MathFloor(t[class="num">0]/class="num">86400);i++)
     {
       if(h[i]>lev_h) lev_h=h[i];
       if(l[i]<lev_l) lev_l=l[i];
     }
   lev_h+=Spread+_Point;
   lev_l-=_Point;
   if(CopyBuffer(hMA,class="num">0,class="num">0,class="num">2,ma)<class="num">2 || CopyBuffer(hCI,class="num">0,class="num">0,class="num">1,atr_h)<class="num">1 || CopyBuffer(hCI,class="num">1,class="num">0,class="num">1,atr_l)<class="num">1)
     {
       Print("Can&class="macro">#x27;t copy indicator buffer!");
       class="kw">return;
     }
   ArraySetAsSeries(ma,true);

遍历持仓改止损止盈的实战写法

这段逻辑跑在 EA 的 OnTick 里,先把点差加进 atr_l[0],再用 for 循环扫一遍 PositionsTotal() 拿到的全部持仓。只挑 Symbol() 与当前图表品种一致的订单动手,避免误改其他品种的仓。 买入仓的处理核心是:若前一根均线 ma[1] 高于开仓价,止损锚在 ma[1],否则退回 lev_l 轨道值。紧接着一层复合判断——原 SL 为 0 或新止损更高、原 TP 为 0 或新止盈更低,且新 SL/TP 距实时 BID 都超过 StopLevel 止损距离,才发 TRADE_ACTION_SLTP 请求。 卖出仓对称处理:ma[1]+Spread 低于开仓价时止损取该值,否则用 lev_h。外汇与贵金属杠杆高、滑点跳空频繁,这种按轨修改法在剧烈行情中可能来不及成交,开 MT5 用策略测试器跑 EURUSD 15 分钟周期能直接看回测里改单失败的次数。 别把止损距离当摆设 StopLevel 是经纪商硬限制,NormalizeDouble 算出来的新 SL 若贴着 BID 减 StopLevel 还是小于等于 0,OrderSend 必被拒。写 EA 时把 StopLevel 打印到日志,比事后猜改单为啥没生效管用。

MQL5 / C++
  atr_l[class="num">0]+=Spread;
class=class="str">"cmt">// 在这个循环中,我们检查所有的持仓
  for(i=class="num">0;i<PositionsTotal();i++)
   {
     class=class="str">"cmt">// 只处理我们自己的交易品种的订单
     if(Symbol()==PositionGetSymbol(i))
      {
       class=class="str">"cmt">// 我们将修改止损和获利值
       request.action=TRADE_ACTION_SLTP;
       class=class="str">"cmt">// 处理买入订单
       if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
        {
         class=class="str">"cmt">// 让我们确定止损
         if(ma[class="num">1]>PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[class="num">1]; else StopLoss=lev_l;
         class=class="str">"cmt">// 如果止损没有被定义或者比所需低
         if((PositionGetDouble(POSITION_SL)==class="num">0 || NormalizeDouble(StopLoss-PositionGetDouble(POSITION_SL),_Digits)>class="num">0
          class=class="str">"cmt">// 如果获利没有被定义或者比所需高
          || PositionGetDouble(POSITION_TP)==class="num">0 || NormalizeDouble(PositionGetDouble(POSITION_TP)-atr_h[class="num">0],_Digits)>class="num">0)
          class=class="str">"cmt">// 新的止损接近当前价格吗?
          && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLoss-StopLevel,_Digits)>class="num">0
          class=class="str">"cmt">// 新的获利接近当前价格吗?
          && NormalizeDouble(atr_h[class="num">0]-SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLevel,_Digits)>class="num">0)
          {
           class=class="str">"cmt">// 设置结构中止损的新值
           request.sl=NormalizeDouble(StopLoss,_Digits);
           class=class="str">"cmt">// 设置结构中获利的新值
           request.tp=NormalizeDouble(atr_h[class="num">0],_Digits);
           class=class="str">"cmt">// 发请求到交易服务器
           OrderSend(request,result);
          }
        }
       class=class="str">"cmt">// 处理卖出订单
       if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
        {
         class=class="str">"cmt">// 让我们确定止损的值
         if(ma[class="num">1]+Spread<PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[class="num">1]+Spread; else StopLoss=lev_h;
         class=class="str">"cmt">// 如果止损没有被定义或者比所需低

「挂单与持仓的止损移动逻辑」

这段逻辑跑在 EA 的 OnTick 里,先扫持仓再扫挂单。对已有仓位,只有当 SL 未设、或当前 SL 距算出的 StopLoss 还有偏差,且 TP 缺失或偏离 atr_l[0] 时,才重新挂单改 SL/TP;同时用 NormalizeDouble 比对 SYMBOL_ASK 与 StopLevel,确保新价不在禁止修改距离内。 持仓修改的核心判断可浓缩成下面这段:SL 或 TP 任一不满足,且新止损距卖价大于 StopLevel、新止盈距卖价也大于 StopLevel,才发 OrderSend。外汇与贵金属点差跳动快,StopLevel 常由经纪商动态给出,实盘里这段若不打日志很容易静默失败。 扫挂单时只处理当前品种。对 BUY_STOP,限定在 StartHour 到 EndHour 交易时段内,且 lev_h < atr_h[0] 才允许移动;开仓价偏低或 SL 未对齐 lev_l 时触发修正。把时间窗和 ATR 阈值写死前,建议先用历史数据看一眼自己品种的波动分布,黄金亚盘常出现 lev_h 假突破。 下面这段是持仓分支的判定与发单代码,逐行看就是条件嵌套加 NormalizeDouble 防浮点误差:

MQL5 / C++
if((PositionGetDouble(POSITION_SL)==class="num">0 || NormalizeDouble(PositionGetDouble(POSITION_SL)-StopLoss,_Digits)>class="num">0
      class=class="str">"cmt">// 如果获利没有被定义或者比所需低
      || PositionGetDouble(POSITION_TP)==class="num">0 || NormalizeDouble(atr_l[class="num">0]-PositionGetDouble(POSITION_TP),_Digits)>class="num">0)
      class=class="str">"cmt">// 新的止损接近当前价格吗?
      && NormalizeDouble(StopLoss-SymbolInfoDouble(Symbol(),SYMBOL_ASK)-StopLevel,_Digits)>class="num">0
      class=class="str">"cmt">// 新的获利接近当前价格吗?
      && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK)-atr_l[class="num">0]-StopLevel,_Digits)>class="num">0)
      {
      class=class="str">"cmt">// 设置结构中止损的新值
      request.sl=NormalizeDouble(StopLoss,_Digits);
      class=class="str">"cmt">// 设置结构中获利的新值
      request.tp=NormalizeDouble(atr_l[class="num">0],_Digits);
      class=class="str">"cmt">// 发请求到交易服务器
      OrderSend(request,result);
      }
      }
      class=class="str">"cmt">// 如果有持仓,从这里退出...
      class="kw">return;
      }
   }
class=class="str">"cmt">// 在这个循环中我们将检查所有的挂单
   for(i=class="num">0;i<OrdersTotal();i++)
   {
   class=class="str">"cmt">// 选择每一个订单,取得订单号
   ticket=OrderGetTicket(i);
   class=class="str">"cmt">// 只处理我们交易品种的订单
   if(OrderGetString(ORDER_SYMBOL)==Symbol())
      {
      class=class="str">"cmt">// 处理止损买单
      if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_STOP)
         {
         class=class="str">"cmt">// 检查是否是交易时间,价格能否移动
         if(dt.hour>=StartHour && dt.hour<EndHour && lev_h<atr_h[class="num">0])
            {
            class=class="str">"cmt">// 如果开仓价位比所需低
            if((NormalizeDouble(lev_h-OrderGetDouble(ORDER_PRICE_OPEN),_Digits)>class="num">0
               class=class="str">"cmt">// 如果止损价位没有定义或者比所需高
               || OrderGetDouble(ORDER_SL)==class="num">0 || NormalizeDouble(OrderGetDouble(ORDER_SL)-lev_l,_Digits)!=class="num">0)
交给小布盯盘看加载后的行为
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 加载后的订单动作与指标绘制是否异常,把重复劳动交给小布,你专注决策。

常见问题

向导只生成包含事件函数和参数声明的空骨架,没有写交易逻辑。需要在 OnTick 里补条件判断和下单函数,才可能触发自动操作。
指标选 Indicator 类型并声明 plot 缓冲,EA 选 Expert Advisor 类型且侧重订单函数;两者模板结构不同,指标偏绘图、EA 偏执行。
可以,小布盯盘的 AIGC 模块能对照品种页回看指标绘制与模拟订单流,减少手动盯加载日志的时间,但外汇贵金属高风险,信号仅作概率参考。
OnInit 做初始化如指标句柄和参数校验,OnTick 在每个报价到来时执行核心逻辑;遗漏前者可能导致句柄无效而报错。
MQL5 分离了持仓、订单与交易历史, netting 与 hedging 模式并存,写 EA 时需按账户类型调用对应交易函数,否则平仓逻辑会失效。