开发一款波段交易入场监控智能交易系统(EA)·进阶篇
「指标句柄与计算缓冲的初始化落点」
在 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 等下一帧更稳。
{
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 可换周期验证。
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,否则你监控的线和标题对不上,信号会偏。
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 分钟图能直观看到缓冲只补新柱。外汇与贵金属杠杆高,指标失效时盲目跟信号可能放大回撤,先在策略测试器验证再上实盘。
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 策略测试器用历史数据验证报警频率。
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。
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); }