开发一款波段交易入场监控智能交易系统(EA)·进阶篇
📘

开发一款波段交易入场监控智能交易系统(EA)·进阶篇

第 2/3 篇

「指标句柄与计算缓冲的初始化落点」

在 MT5 自定义指标里,iMA 句柄创建失败是最常见的初始化坑。上面这段把 NULL 和 PERIOD_CURRENT 传给 iMA,意味着直接绑定当前图表品种与周期;若返回负值,必须用 Print 把 INVALID_HANDLE 和 GetLastError 打出来,否则回测时连哪根线没算出来都难查。 OnCalculate 里第一件事是算 limit = rates_total - prev_calculated,它决定本次只刷多少根新 K 线。prev_calculated 小于 1 时,用 ArrayInitialize 把 Buffer1、Buffer2 填成 EMPTY_VALUE,避免首根乱值;否则 limit++ 把上一根也重算一遍,防止漏掉跨根信号。 数据拷贝顺序有讲究:CopyLow / CopyHigh / CopyTime 任一返回 <=0 就直接 return,不往下走。CopyBuffer(MA_handle,0,0,rates_total,MA) 把均线值拉进数组后,所有序列数组都调 ArraySetAsSeries(...,true) 改成时间倒序,这样 for 循环里 i=0 就是最新一根,和 K 线索引对齐。 主循环从 limit-1 往 0 跑,只处理新增部分。外汇与贵金属波动大、点差跳变频繁,这种增量计算能省 CPU,但高频品种仍可能在 tick 跳空时让 BarsCalculated(MA_handle)<=0,此时返回 0 等下一帧更稳。

MQL5 / C++
  {
    myPoint *= class="num">10;
  }
  MA_handle = iMA(NULL, PERIOD_CURRENT, EMA_Period, class="num">0, MODE_SMA, PRICE_CLOSE);
  if(MA_handle < class="num">0)
  {
    Print("The creation of iMA has failed: MA_handle=", INVALID_HANDLE);
    Print("Runtime error = ", GetLastError());
    class="kw">return(INIT_FAILED);
  }

  class="kw">return(INIT_SUCCEEDED);
}
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 limit = rates_total - prev_calculated;
  class=class="str">"cmt">//--- counting from class="num">0 to rates_total
  ArraySetAsSeries(Buffer1, true);
  ArraySetAsSeries(Buffer2, true);
  class=class="str">"cmt">//--- initial zero
  if(prev_calculated < class="num">1)
    {
      ArrayInitialize(Buffer1, EMPTY_VALUE);
      ArrayInitialize(Buffer2, EMPTY_VALUE);
    }
  else
      limit++;
  class="type">class="kw">datetime Time[];

  if(CopyLow(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, Low) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(Low, true);
  if(BarsCalculated(MA_handle) <= class="num">0)
      class="kw">return(class="num">0);
  if(CopyBuffer(MA_handle, class="num">0, class="num">0, rates_total, MA) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(MA, true);
  if(CopyHigh(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, High) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(High, true);
  if(CopyTime(Symbol(), Period(), class="num">0, rates_total, Time) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(Time, true);
class=class="str">"cmt">//--- main loop
  for(class="type">int i = limit-class="num">1; i >= class="num">0; i--)
    {

◍ EMA100穿叉信号的缓冲区与告警逻辑

这段核心循环负责把价格与 EMA100 的穿越点写进两个独立缓冲区,并在新K线开盘时弹提醒。PLOT_MAXIMUM_BARS_BACK 设为 5000、OMIT_OLDEST_BARS 设为 50,意味着最旧 50 根不参与计算,避免数组越界或拖慢回算。 Buffer1 捕捉的是阴线最低价下穿均线的瞬间:当 Low[i] < MA[i] 且前一根 Low[i+1] > MA[i+1],就在该低价位画箭头;否则写 EMPTY_VALUE 不显示。Buffer2 对称处理最高价上穿均线的情况,颜色用 0x0000FF 蓝箭头区分。 告警只在 i==1(即刚收盘的那根新柱)且 Time[1] 不等于上次告警时间时触发,myAlert 推送“Look for EMA 100 bounce”。外汇与贵金属波动剧烈,EMA100 bounce 仅代表价格回归均线的概率倾向,实盘须自担高风险。 直接把下面代码丢进 MT5 指标文件,加载到 XAUUSD 的 H1 图,就能看到橙/蓝箭头标出的穿越位,调 EMA_Period 可换周期验证。

MQL5 / C++
if (i >= MathMin(PLOT_MAXIMUM_BARS_BACK-class="num">1, rates_total-class="num">1-OMIT_OLDEST_BARS)) class="kw">continue; class=class="str">"cmt">//omit some old rates to prevent "Array out of range" or slow calculation

class=class="str">"cmt">//Indicator Buffer class="num">1
if(Low[i] < MA[i]
&& Low[i+class="num">1] > MA[i+class="num">1] class=class="str">"cmt">//Candlestick Low crosses below Moving Average
)
  {
   Buffer1[i] = Low[i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Look for EMA class="num">100 bounce "); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
else
  {
   Buffer1[i] = EMPTY_VALUE;
   }
class=class="str">"cmt">//Indicator Buffer class="num">2
if(High[i] > MA[i]
&& High[i+class="num">1] < MA[i+class="num">1] class=class="str">"cmt">//Candlestick High crosses above Moving Average
)
  {
   Buffer2[i] = High[i]; class=class="str">"cmt">//Set indicator value at Candlestick High
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Look for EMA class="num">100 bounce "); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
else
  {
   Buffer2[i] = EMPTY_VALUE;
   }
 }
 class="kw">return(rates_total);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Clemence Benjamin"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.0"
class="macro">#class="kw">property description "EMA class="num">100 Monitoring Indicator"
class=class="str">"cmt">//--- indicator settings
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers class="num">2
class="macro">#class="kw">property indicator_plots   class="num">2
class="macro">#class="kw">property indicator_type1   DRAW_ARROW
class="macro">#class="kw">property indicator_width1  class="num">5
class="macro">#class="kw">property indicator_color1  0xFFAA00
class="macro">#class="kw">property indicator_label1  "Look for EMA class="num">100 bounce "
class="macro">#class="kw">property indicator_type2   DRAW_ARROW
class="macro">#class="kw">property indicator_width2  class="num">5
class="macro">#class="kw">property indicator_color2  0x0000FF
class="macro">#class="kw">property indicator_label2  "Look for EMA class="num">100 bounce "
class="macro">#define PLOT_MAXIMUM_BARS_BACK class="num">5000
class="macro">#define OMIT_OLDEST_BARS class="num">50
class=class="str">"cmt">//--- indicator buffers
class="type">class="kw">double Buffer1[];
class="type">class="kw">double Buffer2[];
input class="type">int EMA_Period = class="num">100;
class="type">class="kw">datetime time_alert; class=class="str">"cmt">//used when sending alert
class="type">bool Audible_Alerts = true;

把 EMA 监控的报警通道先搭起来

这段初始化与报警函数,是 EMA 100 监控指标能跑起来的骨架。先声明全局开关 Push_Notifications 为 true,决定要不要往手机推通知;myPoint 在 OnInit 里按品种小数位修正,遇到 5 位或 3 位报价就乘 10,保证后面距离计算用的是「标准点」而非裸 Point。 myAlert 按 type 分流:print 只走 Print 到日志;error 和 indicator 都带「EMA 100 Monitor @ 品种,周期」前缀打印,indicator 还会在 Audible_Alerts 开时弹 Alert、Push_Notifications 开时发 SendNotification。order 和 modify 分支先空着,留给你接下单或改仓逻辑。 OnInit 里给两个箭头缓冲 Buffer1、Buffer2 绑定索引 0 和 1,箭头码分别设 233、234;DRAW_BEGIN 用 MathMax(Bars-P LOT_MAXIMUM_BARS_BACK+1, OMIT_OLDEST_BARS+1) 掐掉太老的历史。MA_handle 用 iMA 取当前周期收盘价 SMA(注意代码里写的是 MODE_SMA,变量名却叫 EMA,复制时得自己改周期参数或平滑模式)。外汇和贵金属波动大,这类监控只作概率参考,实盘前务必在 MT5 策略测试器跑一遍。 别把正态当圣经:iMA 这里若真想要指数平滑,MODE_SMA 得换成 MODE_EMA,否则你监控的线和标题对不上,信号会偏。

MQL5 / C++
class="type">bool Push_Notifications = true;
class="type">class="kw">double myPoint; class=class="str">"cmt">//initialized in OnInit
class="type">class="kw">double Low[];
class="type">int MA_handle;
class="type">class="kw">double MA[];
class="type">class="kw">double High[];
class="type">void myAlert(class="type">class="kw">string type, class="type">class="kw">string message)
  {
   if(type == "print")
       Print(message);
   else if(type == "error")
     {
       Print(type+" | EMA class="num">100 Monitor @ "+Symbol()+","+IntegerToString(Period())+" | "+message);
     }
   else if(type == "order")
     {
     }
   else if(type == "modify")
     {
     }
   else if(type == "indicator")
     {
       Print(type+" | EMA class="num">100 Monitor @ "+Symbol()+","+IntegerToString(Period())+" | "+message);
       if(Audible_Alerts) Alert(type+" | EMA class="num">100 Monitor @ "+Symbol()+","+IntegerToString(Period())+" | "+message);
       if(Push_Notifications) SendNotification(type+" | EMA class="num">100 Monitor @ "+Symbol()+","+IntegerToString(Period())+" | "+message);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {  
   SetIndexBuffer(class="num">0, Buffer1);
   PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1));
   PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">233);
   SetIndexBuffer(class="num">1, Buffer2);
   PlotIndexSetDouble(class="num">1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(class="num">1, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1));
   PlotIndexSetInteger(class="num">1, PLOT_ARROW, class="num">234);
   class=class="str">"cmt">//initialize myPoint
   myPoint = Point();
   if(Digits() == class="num">5 || Digits() == class="num">3)
     {
       myPoint *= class="num">10;
     }
   MA_handle = iMA(NULL, PERIOD_CURRENT, EMA_Period, class="num">0, MODE_SMA, PRICE_CLOSE);
   if(MA_handle < class="num">0)
     {

「指标初始化失败与 OnCalculate 的数据搬运」

指标句柄拿不到时,直接把 INVALID_HANDLE 和 GetLastError() 打进日志并返回 INIT_FAILED,MT5 终端诊断栏会立刻暴露问题根源,比运行时莫名空白线好查得多。 OnCalculate 里第一件事是算 limit = rates_total - prev_calculated,只在新增 K 线时跑新数据;prev_calculated 小于 1 时用 ArrayInitialize 把 Buffer1、Buffer2 填成 EMPTY_VALUE,避免首根乱值。 真正干活前要把 Low、High、MA、Time 全部 ArraySetAsSeries(true) 改成时间序列,否则用 i 下标取最近价会整反。CopyLow / CopyHigh / CopyBuffer / CopyTime 任一个返回 <=0 就直接 return,省得拿残数据画指标。 主循环从 limit-1 往 0 倒推,这种写法贴合 MT5 序列数组习惯,复制去跑 EURUSD 15 分钟图能直观看到缓冲只补新柱。外汇与贵金属杠杆高,指标失效时盲目跟信号可能放大回撤,先在策略测试器验证再上实盘。

MQL5 / C++
Print("The creation of iMA has failed: MA_handle=", INVALID_HANDLE);
Print("Runtime error = ", GetLastError());
class="kw">return(INIT_FAILED);
}

class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
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 limit = rates_total - prev_calculated;
   class=class="str">"cmt">//--- counting from class="num">0 to rates_total
   ArraySetAsSeries(Buffer1, true);
   ArraySetAsSeries(Buffer2, true);
   class=class="str">"cmt">//--- initial zero
   if(prev_calculated < class="num">1)
     {
      ArrayInitialize(Buffer1, EMPTY_VALUE);
      ArrayInitialize(Buffer2, EMPTY_VALUE);
     }
   else
      limit++;
   class="type">class="kw">datetime Time[];

   if(CopyLow(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, Low) <= class="num">0) class="kw">return(rates_total);
   ArraySetAsSeries(Low, true);
   if(BarsCalculated(MA_handle) <= class="num">0)
      class="kw">return(class="num">0);
   if(CopyBuffer(MA_handle, class="num">0, class="num">0, rates_total, MA) <= class="num">0) class="kw">return(rates_total);
   ArraySetAsSeries(MA, true);
   if(CopyHigh(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, High) <= class="num">0) class="kw">return(rates_total);
   ArraySetAsSeries(High, true);
   if(CopyTime(Symbol(), Period(), class="num">0, rates_total, Time) <= class="num">0) class="kw">return(rates_total);
   ArraySetAsSeries(Time, true);
   class=class="str">"cmt">//--- main loop
   for(class="type">int i = limit-class="num">1; i >= class="num">0; i--)
     {

◍ EMA100 穿越检测与多通道报警落地

这段逻辑干两件事:在 K 线最低价下穿均线时给 Buffer1 打点,最高价上穿均线时给 Buffer2 打点,并在下一根 bar 开盘时触发一次报警。循环开头用 MathMin(PLOT_MAXIMUM_BARS_BACK-1, rates_total-1-OMIT_OLDEST_BARS) 跳过最老的数据,避免数组越界或回算过慢——这是 MT5 自定义指标里常见的防崩写法。 Low[i] < MA[i] 且 Low[i+1] > MA[i+1] 判定为向下交叉,High[i] > MA[i] 且 High[i+1] < MA[i+1] 判定为向上交叉;非交叉位置写 EMPTY_VALUE,不污染缓冲区。报警只在 i==1 且 Time[1] 不等于已记录 time_alert 时发,保证每根新 bar 最多响一次。 外层的 input 参数把报警拆成终端、推送、Telegram 三路:EnableTelegramAlerts 默认 false,填了 BotToken 和 ChatID 才走 SendTelegramMessage,URL 拼接用 5000 毫秒超时。外汇与贵金属波动大、滑点频繁,EMA100 反弹信号仅作概率参考,实盘前请在 MT5 策略测试器用历史数据验证报警频率。

MQL5 / C++
if (i >= MathMin(PLOT_MAXIMUM_BARS_BACK-class="num">1, rates_total-class="num">1-OMIT_OLDEST_BARS)) class="kw">continue; class=class="str">"cmt">//omit some old rates to prevent "Array out of range" or slow calculation

class=class="str">"cmt">//Indicator Buffer class="num">1
if(Low[i] < MA[i]
&& Low[i+class="num">1] > MA[i+class="num">1] class=class="str">"cmt">//Candlestick Low crosses below Moving Average
)
  {
   Buffer1[i] = Low[i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Look for EMA class="num">100 bounce "); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
else
  {
   Buffer1[i] = EMPTY_VALUE;
   }
class=class="str">"cmt">//Indicator Buffer class="num">2
if(High[i] > MA[i]
&& High[i+class="num">1] < MA[i+class="num">1] class=class="str">"cmt">//Candlestick High crosses above Moving Average
)
  {
   Buffer2[i] = High[i]; class=class="str">"cmt">//Set indicator value at Candlestick High
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Look for EMA class="num">100 bounce "); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
else
  {
   Buffer2[i] = EMPTY_VALUE;
   }
   }
 class="kw">return(rates_total);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">class="kw">string IndicatorName = "ema100_monitoring_indicator"; class=class="str">"cmt">// Name of the custom indicator
input class="type">bool EnableTerminalAlerts = true;
input class="type">bool EnablePushNotifications = true;
input class="type">bool EnableTelegramAlerts = false;
input class="type">class="kw">string TelegramBotToken = "YOUR_BOT_TOKEN"; class=class="str">"cmt">// Replace with your bot token
input class="type">class="kw">string TelegramChatID = "YOUR_CHAT_ID";     class=class="str">"cmt">// Replace with your chat ID
class="type">int indicatorHandle = INVALID_HANDLE;
class="type">int emaHandle = INVALID_HANDLE;
class="type">void AlertMessage(class="type">class="kw">string message) {
   if (EnableTerminalAlerts) Alert(message);
   if (EnablePushNotifications) SendNotification(message);
   if (EnableTelegramAlerts) SendTelegramMessage(message);
}
class="type">void SendTelegramMessage(class="type">class="kw">string message) {
   if (EnableTelegramAlerts) {
      class="type">class="kw">string url = "https:class=class="str">"cmt">//api.telegram.org/bot" + TelegramBotToken + "/sendMessage?chat_id=" + TelegramChatID + "&text=" + message;
      class="type">int timeout = class="num">5000;
      ResetLastError();
      class="type">char postData[];
      class="type">uchar result[];
      class="type">class="kw">string response;

把监控逻辑跑进 MT5 生命周期

EA 的初始化阶段先把自定义指标和内置 EMA100 挂上当前图表。indicatorHandle 用 iCustom 绑定 IndicatorName,emaHandle 用 iMA 取 100 周期收盘价指数均线;任一返回 INVALID_HANDLE 就直接 INIT_FAILED,避免后续空指针乱报。 ChartIndicatorAdd(0,0,emaHandle) 把 EMA100 画到主图,失败仅 Print 不中断——图表上看不到线但逻辑照跑。OnDeinit 里用 IndicatorRelease 释放两个句柄,EA 卸载时不留垃圾句柄。 OnTick 用 static datetime lastAlertTime 拦掉同根 K 线的重复信号,这是实盘里容易被忽略的刷屏源。CopyBuffer 对自定义指标的两个缓冲区各取 1 根且按时间倒序(ArraySetAsSeries true),任一拷贝失败立即 return,不误判。 WebRequest 发 Telegram 那段,res != 200 时把 GetLastError 和 HTTP 状态码一起打出来,能直接区分是 MT5 网络权限没开还是机器人 token 错。下面这段是原文里 WebRequest 调用与回执判断的核心片段,逐行看一遍就知道该在哪儿插自己的 url 和 postData。

MQL5 / C++
class="type">int res = WebRequest("GET", url, NULL, timeout, postData, result, response);
if (res != class="num">200) {
   Print("Telegram WebRequest failed. Error: ", GetLastError(), ", HTTP Code: ", res);
} else {
   Print("Telegram message sent successfully: ", response);
}

常见问题

句柄必须在 OnInit 里用 iMA 创建,不要在 OnCalculate 反复申请;失败立即返回 INIT_FAILED 并打印日志。
检查 CopyBuffer 的索引和起始位是否对齐,且信号判断放在当前未闭合K线之前要留缓冲,避免未来函数。
可以,小布盯盘已内置均线穿越诊断,打开对应品种页就能看到实时穿叉告警,不用自己写监控。
用独立布尔标记区分每个通道状态,触发后先置位再发通知,复位要在反向条件明确满足时做。
多是句柄无效或图表品种周期不匹配,先在 OnInit 校验句柄并确认当前图表周期和指标参数一致。