构建K线趋势约束模型(第五部分):通知系统(第三部分)·进阶篇
(2/3)·手动刷指标漏掉信号?用API把约束模型警报直送27亿用户的聊天框
指标外壳与报警冷却的初始化
这段声明定义了指标第 5、6 条绘图的视觉属性:第 5 条画橙色(0xFFAA00)标签「Buy」,第 6 条画蓝色(0x0000FF)实线宽 2 标签「Sell」,用于把多空信号直接叠在图表上。回看深度限制为 PLOT_MAXIMUM_BARS_BACK=5000,最旧 50 根柱被省略,避免历史缓冲爆掉。 输入参数里 RSI 超卖 30、超买 70,慢均线 200、快均线 100,构成趋势约束的基础阈值; audible 与 push 开关默认全开,说明信号既弹窗也推手机。 报警系统用 last_alert_time 配合 alert_cooldown_seconds=60 做冷却:60 秒内重复触发直接 return,防止一根毛刺 K 线连发十几条提醒。myAlert 拼出的前缀写死「Trend Constraint V1.06 @ 品种,周期」,你复制代码后若想区分多版本,得先改这串字。 ShellExecuteW 从 shell32.dll 导入,后续可能用来调外部程序;目前看只是预留接口,没在 myAlert 前半段调用。开 MT5 把这段贴进自定义指标头部,编译后先观察 Buy/Sell 线颜色与冷却间隔是否符合你的盯盘节奏。
class="macro">#class="kw">property indicator_color5 0xFFAA00 class="macro">#class="kw">property indicator_label5 "Buy" class="macro">#class="kw">property indicator_type6 DRAW_LINE class="macro">#class="kw">property indicator_style6 STYLE_SOLID class="macro">#class="kw">property indicator_width6 class="num">2 class="macro">#class="kw">property indicator_color6 0x0000FF class="macro">#class="kw">property indicator_label6 "Sell" 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[]; class="type">class="kw">double Buffer3[]; class="type">class="kw">double Buffer4[]; class="type">class="kw">double Buffer5[]; class="type">class="kw">double Buffer6[]; input class="type">class="kw">double Oversold = class="num">30; input class="type">class="kw">double Overbought = class="num">70; input class="type">int Slow_MA_period = class="num">200; input class="type">int Fast_MA_period = class="num">100; class="type">class="kw">datetime time_alert; class=class="str">"cmt">//used when sending alert input class="type">bool Audible_Alerts = true; input class="type">bool Push_Notifications = true; class="type">class="kw">double myPoint; class=class="str">"cmt">//initialized in OnInit class="type">int RSI_handle; class="type">class="kw">double RSI[]; class="type">class="kw">double Open[]; class="type">class="kw">double Close[]; class="type">int MA_handle; class="type">class="kw">double MA[]; class="type">int MA_handle2; class="type">class="kw">double MA2[]; class="type">int MA_handle3; class="type">class="kw">double MA3[]; class="type">int MA_handle4; class="type">class="kw">double MA4[]; class="type">class="kw">double Low[]; class="type">class="kw">double High[]; class="type">int MA_handle5; class="type">class="kw">double MA5[]; class="type">int MA_handle6; class="type">class="kw">double MA6[]; class="type">int MA_handle7; class="type">class="kw">double MA7[]; class=class="str">"cmt">//--- ShellExecuteW declaration ---------------------------------------------- class="macro">#class="kw">import "shell32.dll" class="type">int ShellExecuteW(class="type">int hwnd, class="type">class="kw">string lpOperation, class="type">class="kw">string lpFile, class="type">class="kw">string lpParameters, class="type">class="kw">string lpDirectory, class="type">int nShowCmd); class="macro">#class="kw">import class=class="str">"cmt">//--- global variables ------------------------------------------------------ class="type">class="kw">datetime last_alert_time; input class="type">int alert_cooldown_seconds = class="num">60; class=class="str">"cmt">// Cooldown period in seconds class=class="str">"cmt">//--- myAlert function ------------------------------------------------------ class="type">void myAlert(class="type">class="kw">string type, class="type">class="kw">string message) { class="type">class="kw">datetime current_time = TimeCurrent(); if (current_time - last_alert_time < alert_cooldown_seconds) { class=class="str">"cmt">// Skip alert if within cooldown period class="kw">return; } last_alert_time = current_time; class="type">class="kw">string full_message = type + " | Trend Constraint V1.class="num">06 @ " + Symbol() + "," + IntegerToString(Period()) + " | " + message; if (type == "print") { Print(message); } else if (type == "error") { Print(type + " | Trend Constraint V1.class="num">06 @ " + Symbol() + "," + IntegerToString(Period()) + " | " + message); } else if (type == "order") {
「把指标信号推到 WhatsApp 与 MT5 终端」
当信号类型落在 indicator 或 info 分支时,脚本会先按 Audible_Alerts、Push_Notifications 两个开关决定是否走 MT5 内置的 Alert 与 SendNotification。若两者开启,终端弹窗与手机推送会同时触发,适合盯盘时不在电脑前的场景。
接着是一段调用外部 Python 的发消息逻辑:硬编码了 Python 3.12 的解释器路径与 send_whatsapp_message.py 脚本路径,用 ShellExecuteW 拉起 cmd.exe 执行拼接好的命令,并追加 && timeout 5 留 5 秒让进程跑完。返回码 ≤32 视为失败,会打印 GetLastError 的具体错误码;否则打印成功返回码。
OnInit 里给两个缓冲区绑了箭头样式:Buffer1 用 241(▲)、Buffer2 用 242(▼),并通过 PlotIndexSetInteger 的 PLOT_DRAW_BEGIN 控制从哪根 K 线开始画,计算式取 Bars 减去回看限制与省略最老 K 线数的较大值。外汇与贵金属波动剧烈,这类外部调用若路径写死在别人机器上会直接报错,部署前务必改成本机实际路径。
class=class="str">"cmt">// Add order alert handling if needed } else if (type == "modify") { class=class="str">"cmt">// Add modify alert handling if needed } else if (type == "indicator" || type == "info") { if (Audible_Alerts) { Alert(full_message); } if (Push_Notifications) { SendNotification(full_message); } class=class="str">"cmt">// Send to WhatsApp class="type">class="kw">string python_path = "C:\\Users\\****\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"; class="type">class="kw">string script_path = "C:\\Users\\****\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\send_whatsapp_message.py"; class="type">class="kw">string command = python_path + " \"" + script_path + "\" \"" + full_message + "\""; class=class="str">"cmt">// Debugging: Print the command being executed Print("Executing command to send WhatsApp message: ", command); class=class="str">"cmt">// Use cmd.exe to execute the command and then wait for class="num">5 seconds class="type">class="kw">string final_command = "/c " + command + " && timeout class="num">5"; class="type">int result = ShellExecuteW(class="num">0, "open", "cmd.exe", final_command, NULL, class="num">1); if (result <= class="num">32) { class="type">int error_code = GetLastError(); Print("Failed to execute Python script. Error code: ", error_code); } else { Print("Successfully executed Python script. Result code: ", result); } } } 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">241); 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">242);
◍ 缓冲绑定与指标句柄的初始化落点
这段初始化代码把第 2~5 号指标缓冲依次绑到 Buffer3~Buffer6,并给 2 号和 3 号缓冲指定了箭头样式 236、238,其余缓冲只设了空值与起绘位置。起绘位置统一用 MathMax(Bars-SHIFT+1, OMIT+1) 控制,避免在最旧 K 线上误绘——外汇与贵金属品种历史深度不同,这个边界值最好按实盘品种手调。 SetIndexBuffer(2, Buffer3); PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+1, OMIT_OLDEST_BARS+1)); PlotIndexSetInteger(2, PLOT_ARROW, 236); // 以上四行:2号缓冲绑 Buffer3,空值 EMPTY_VALUE,起绘位置取边界最大值,箭头代码 236(↓类符号)。 SetIndexBuffer(3, Buffer4); PlotIndexSetInteger(3, PLOT_ARROW, 238); // 3号缓冲绑 Buffer4,箭头 238(↑类符号),其余设置同 2 号。 SetIndexBuffer(4, Buffer5); SetIndexBuffer(5, Buffer6); // 4、5 号缓冲仅绑数据,无箭头,用作辅助线或状态记录。 myPoint 的处理值得留意:当 Digits() 返回 5 或 3(即报价尾随一位小数点的黄金/交叉盘常见),myPoint 乘 10,把点值对齐到真正的最小波动。RSI 周期写死 14、MA 用 7 期 SMMA 与 400 期 SMA,句柄创建失败会直接 Print 错误码并返回 INIT_FAILED,开 MT5 挂这个 EA 时若日志出现 iRSI/iMA failed,优先查品种是否支持当前周期。
SetIndexBuffer(class="num">2, Buffer3); PlotIndexSetDouble(class="num">2, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(class="num">2, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1)); PlotIndexSetInteger(class="num">2, PLOT_ARROW, class="num">236); SetIndexBuffer(class="num">3, Buffer4); PlotIndexSetDouble(class="num">3, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(class="num">3, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1)); PlotIndexSetInteger(class="num">3, PLOT_ARROW, class="num">238); SetIndexBuffer(class="num">4, Buffer5); PlotIndexSetDouble(class="num">4, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(class="num">4, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1)); SetIndexBuffer(class="num">5, Buffer6); PlotIndexSetDouble(class="num">5, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(class="num">5, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1)); class=class="str">"cmt">// Send test message on launch myAlert("info", "Thank you for subscribing. You shall be receiving Trend Constraint signal alerts via Whatsapp."); class=class="str">"cmt">//initialize myPoint myPoint = Point(); if(Digits() == class="num">5 || Digits() == class="num">3) { myPoint *= class="num">10; } RSI_handle = iRSI(NULL, PERIOD_CURRENT, class="num">14, PRICE_CLOSE); if(RSI_handle < class="num">0) { Print("The creation of iRSI has failed: RSI_handle=", INVALID_HANDLE); Print("Runtime error = ", GetLastError()); class="kw">return(INIT_FAILED); } MA_handle = iMA(NULL, PERIOD_CURRENT, class="num">7, class="num">0, MODE_SMMA, 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); } MA_handle2 = iMA(NULL, PERIOD_CURRENT, class="num">400, class="num">0, MODE_SMA, PRICE_CLOSE); if(MA_handle2 < class="num">0) {
均线句柄的批量创建与失败拦截
在指标初始化阶段,连续调用 iMA 拿到 5 条以上均线句柄是常见做法。下面这段把 100 周期 EMA、200 周期 SMA、200 周期 EMA 以及两组外部传入周期(Fast_MA_period / Slow_MA_period)的 SMA 全部挂在当前图表周期与收盘价上。 每个句柄拿回来必须立刻判 <0:一旦返回 INVALID_HANDLE,就打印具体句柄名和 GetLastError() 的运行错误码,然后 return(INIT_FAILED)。不拦截的话,后续 OnCalculate 里 CopyBuffer 会直接读空句柄,MT5 终端可能静默算不出值。 注意 MA_handle3 用 100/0/MODE_EMA,MA_handle4 用 200/0/MODE_SMA,MA_handle7 又用 200/0/MODE_EMA——同周期不同算法共存时,句柄变量名千万别写串。开 MT5 把这段贴进 OnInit,故意改一个负周期参数,能在专家日志里看到对应 Runtime error 号。
MA_handle3 = iMA(NULL, PERIOD_CURRENT, class="num">100, class="num">0, MODE_EMA, PRICE_CLOSE); if(MA_handle3 < class="num">0) { Print("The creation of iMA has failed: MA_handle3=", INVALID_HANDLE); Print("Runtime error = ", GetLastError()); class="kw">return(INIT_FAILED); } MA_handle4 = iMA(NULL, PERIOD_CURRENT, class="num">200, class="num">0, MODE_SMA, PRICE_CLOSE); if(MA_handle4 < class="num">0) { Print("The creation of iMA has failed: MA_handle4=", INVALID_HANDLE); Print("Runtime error = ", GetLastError()); class="kw">return(INIT_FAILED); } MA_handle5 = iMA(NULL, PERIOD_CURRENT, Fast_MA_period, class="num">0, MODE_SMA, PRICE_CLOSE); if(MA_handle5 < class="num">0) { Print("The creation of iMA has failed: MA_handle5=", INVALID_HANDLE); Print("Runtime error = ", GetLastError()); class="kw">return(INIT_FAILED); } MA_handle6 = iMA(NULL, PERIOD_CURRENT, Slow_MA_period, class="num">0, MODE_SMA, PRICE_CLOSE); if(MA_handle6 < class="num">0) { Print("The creation of iMA has failed: MA_handle6=", INVALID_HANDLE); Print("Runtime error = ", GetLastError()); class="kw">return(INIT_FAILED); } MA_handle7 = iMA(NULL, PERIOD_CURRENT, class="num">200, class="num">0, MODE_EMA, PRICE_CLOSE); if(MA_handle7 < class="num">0) { Print("The creation of iMA has failed: MA_handle7=", 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(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated,
「多周期映射与缓冲初始化的坑」
指标 OnCalculate 入口先把六个输出缓冲全部设成序列数组(ArraySetAsSeries 置 true),否则按索引 0 取最新值会错位。第一次运行 prev_calculated 小于 1 时,必须用 ArrayInitialize 把 Buffer1~Buffer6 填成 EMPTY_VALUE,避免历史柱上残留 0 被画图当成有效信号。 多周期对齐靠 iBarShift 把当前图每根柱的时间戳映射到 M1 与 D1 的柱序号:barshift_M1[i] 和 barshift_D1[i] 分别存偏移量,循环覆盖 0 到 rates_total-1。若 CopyTime 取不到 TimeShift 直接 return rates_total,指标会原地重算不推进。 RSI 句柄校验用 BarsCalculated(RSI_handle)<=0 就 return 0,CopyBuffer 取不到也回退 rates_total。M1 的开盘价通过 CopyOpen(Symbol(),PERIOD_M1,0,rates_total,Open) 拉进来并设序列。外汇与贵金属波动受杠杆与跳空影响,这类跨周期调用在流动性稀薄时段可能返回无效偏移,实盘前建议在 MT5 用 EURUSD 的 M5 图表跑一遍看 limit 与 barshift 数组是否越界。
class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">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); ArraySetAsSeries(Buffer3, true); ArraySetAsSeries(Buffer4, true); ArraySetAsSeries(Buffer5, true); ArraySetAsSeries(Buffer6, true); class=class="str">"cmt">//--- initial zero if(prev_calculated < class="num">1) { ArrayInitialize(Buffer1, EMPTY_VALUE); ArrayInitialize(Buffer2, EMPTY_VALUE); ArrayInitialize(Buffer3, EMPTY_VALUE); ArrayInitialize(Buffer4, EMPTY_VALUE); ArrayInitialize(Buffer5, EMPTY_VALUE); ArrayInitialize(Buffer6, EMPTY_VALUE); } else limit++; class="type">class="kw">datetime Time[]; class="type">class="kw">datetime TimeShift[]; if(CopyTime(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, TimeShift) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(TimeShift, true); class="type">int barshift_M1[]; ArrayResize(barshift_M1, rates_total); class="type">int barshift_D1[]; ArrayResize(barshift_D1, rates_total); for(class="type">int i = class="num">0; i < rates_total; i++) { barshift_M1[i] = iBarShift(Symbol(), PERIOD_M1, TimeShift[i]); barshift_D1[i] = iBarShift(Symbol(), PERIOD_D1, TimeShift[i]); } if(BarsCalculated(RSI_handle) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(RSI_handle, class="num">0, class="num">0, rates_total, RSI) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(RSI, true); if(CopyOpen(Symbol(), PERIOD_M1, class="num">0, rates_total, Open) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(Open, true);
◍ 多均线句柄的缓冲区拷贝与序列对齐
指标计算开头这一段,本质是把日线收盘价、当前周期高低价、以及 7 条均线(MA_handle 到 MA_handle7)的缓冲区一次性拉进本地数组。任何一次 Copy 或 BarsCalculated 返回小于等于 0,就直接 return,避免后续用空数组算出鬼画符。 日线收盘价用 CopyClose(Symbol(), PERIOD_D1, 0, rates_total, Close),当前周期高低价则分别用 CopyLow / CopyHigh 配 PERIOD_CURRENT。时间轴用 CopyTime(Symbol(), Period(), 0, rates_total, Time) 取,注意 Period() 返回的就是当前图表周期枚举。 每条均线都先判 BarsCalculated(handle) 是否就绪,再用 CopyBuffer(handle, 0, 0, rates_total, MAx) 把主缓冲拷进来。7 条线意味着最少 7 次句柄有效性检查——若你只挂了 3 条均线却写了 7 个 handle,编译不报错但运行会卡在 return(0)。 所有数组拷完都接一句 ArraySetAsSeries(..., true),把索引 0 钉在最新一根 K 线上。这样写 MA[0]、Close[0] 指的就是当前 bar,回测和实时计算才不会错位。外汇与贵金属杠杆高,这类多缓冲指标在跳空时可能拷贝不完整,上 MT5 用样例品种跑一遍才能确认边界行为。
if(CopyClose(Symbol(), PERIOD_D1, class="num">0, rates_total, Close) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(Close, 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(BarsCalculated(MA_handle2) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(MA_handle2, class="num">0, class="num">0, rates_total, MA2) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(MA2, true); if(BarsCalculated(MA_handle3) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(MA_handle3, class="num">0, class="num">0, rates_total, MA3) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(MA3, true); if(BarsCalculated(MA_handle4) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(MA_handle4, class="num">0, class="num">0, rates_total, MA4) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(MA4, true); if(CopyLow(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, Low) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(Low, true); if(CopyHigh(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, High) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(High, true); if(BarsCalculated(MA_handle5) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(MA_handle5, class="num">0, class="num">0, rates_total, MA5) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(MA5, true); if(BarsCalculated(MA_handle6) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(MA_handle6, class="num">0, class="num">0, rates_total, MA6) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(MA6, true); if(BarsCalculated(MA_handle7) <= class="num">0) class="kw">return(class="num">0); if(CopyBuffer(MA_handle7, class="num">0, class="num">0, rates_total, MA7) <= class="num">0) class="kw">return(rates_total); ArraySetAsSeries(MA7, 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