构建K线趋势约束模型(第5部分):通知系统(第一部分)·进阶篇
(2/3)· 终端、推送、邮件三件套只是起点,Telegram与WhatsApp集成才是离场也能盯盘的关键一步
「指标骨架与多缓冲区的初始化」
这段代码定义了一个带趋势约束的自定义指标基础结构,核心是先声明 6 个绘图缓冲区和若干输入参数。OMIT_OLDEST_BARS 被定为 50,意味着图表最旧的那 50 根 K 线不参与绘制,避免历史远端噪声干扰信号显示。 输入参数里 RSI 的超卖、超买线分别是 30 和 70,快慢均线周期为 100 和 200,这组默认值在外汇与贵金属的 H1 及以上周期常用来过滤假突破,但高杠杆下仍属高风险配置,需自行回测验证。 myAlert 函数把提示分成 print、error、indicator 等类型,其中 indicator 类在 Audible_Alerts 与 Push_Notifications 均为 true 时会同时弹窗报警并推送手机通知,方便盯盘时脱离屏幕。 OnInit 里对 Buffer1 调用 SetIndexBuffer(0,...) 并设 PLOT_ARROW 为 241(即向上箭头字符),同时用 PlotIndexSetInteger 控制绘制起点,计算逻辑是 Max(Bars-POST_MAXIMUM_BARS_BACK+1, 51),保证前面省略的 50 根不被画出来。
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[]; class="kw">input class="type">class="kw">double Oversold = class="num">30; class="kw">input class="type">class="kw">double Overbought = class="num">70; class="kw">input class="type">int Slow_MA_period = class="num">200; class="kw">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 class="kw">input class="type">bool Audible_Alerts = true; class="kw">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">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+" | Trend Constraint V1.class="num">04 @ "+Symbol()+","+IntegerToString(Period())+" | "+message); } else if(type == "order") { } else if(type == "modify") { } else if(type == "indicator") { if(Audible_Alerts) Alert(type+" | Trend Constraint V1.class="num">04 @ "+Symbol()+","+IntegerToString(Period())+" | "+message); if(Push_Notifications) SendNotification(type+" | Trend Constraint V1.class="num">04 @ "+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">241); SetIndexBuffer(class="num">1, Buffer2); PlotIndexSetDouble(class="num">1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
◍ 缓冲区与箭头的初始化落点
这段初始化逻辑把指标的第 1~5 号绘图索引分别绑到不同缓冲区,并各自指定了箭头样式与起始绘制的 K 线位置。注意 PLOT_DRAW_BEGIN 统一用 MathMax(Bars()-PLOT_MAXIMUM_BARS_BACK+1, OMIT_OLDEST_BARS+1) 计算,意味着旧数据超出回看窗口的部分会被主动跳过,避免在太靠前的历史棒上画信号。 箭头代码里 242、236、238 分别对应 Wingdings 字体下的特定符号,挂在索引 1/2/3 上;索引 4 和 5 只设了空值与绘制起点,没指定箭头,通常留给线型或隐藏缓冲。复制时若箭头不显示,先确认图表所用的 Wingdings 符号映射是否和代码里的数字一致。 myPoint 的处理针对 5 位或 3 位报价品种乘以 10,把点值归一化到 4 位/2 位口径,外汇与贵金属报价差异容易导致止损计算偏差,这一步不能省。 RSI 与 MA 句柄的创建放在尾部:RSI 用 14 周期收盘价,MA 用 7 周期零偏移 SMMA(平滑移动平均)。两者若返回负值会直接打印错误码并 INIT_FAILED,开 MT5 加载时若指标空白,先去专家日志看是不是句柄创建就失败了。
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); 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">//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); }
多周期均线句柄的初始化与容错
在 MT5 自定义指标里,均线句柄必须在 OnInit 中一次性建好,否则后续 OnCalculate 取不到缓冲区。下面这段代码连续创建了 6 条均线:400 期 SMA、100 期 EMA、200 期 SMA,以及用外部参数传入周期的 Fast/Slow 两条 SMA,全部基于当前图表周期和收盘价。 每条 iMA 调用后都立刻判断返回值是否小于 0。MQL5 里句柄创建失败会返回 INVALID_HANDLE(即 -1),此时打印错误码并 return(INIT_FAILED),指标直接终止加载,避免在残缺状态下跑计算。 值得注意,100 期那条用的是 MODE_EMA,其余 SMA 占多数。这种混合均线结构在价格行为过滤中常见:长周期 SMA 定趋势边界,短 EMA 抓拐点脉冲。外汇与贵金属波动剧烈,这类组合仅作概率参考,实盘前务必在策略测试器跑一遍不同品种验证句柄稳定性。 OnCalculate 的函数签名保留了标准参数列:rates_total、prev_calculated 及 OHLC 引用数组。虽然本节没写计算体,但句柄建好且 INIT_SUCCEEDED 返回,才算拿到进计算环节的门票。
MA_handle2 = iMA(NULL, PERIOD_CURRENT, class="num">400, class="num">0, MODE_SMA, PRICE_CLOSE); if(MA_handle2 < class="num">0) { Print("The creation of iMA has failed: MA_handle2=", INVALID_HANDLE); Print("Runtime error = ", GetLastError()); class="kw">return(INIT_FAILED); } 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); } 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, 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[],
「指标句柄数据拉取与缓冲初始化」
这段代码是自定义指标 OnCalculate 里的数据准备段,核心是把 RSI 与多组 MA 的句柄数据搬进本地数组,并给六个绘图缓冲做时序倒置。 先算 limit = rates_total - prev_calculated,只在首次计算(prev_calculated < 1)时用 ArrayInitialize 把 Buffer1~6 全填 EMPTY_VALUE,避免历史残留;非首次则 limit++ 只刷新增量根数。 六个缓冲都调 ArraySetAsSeries(..., true),意味着下标 0 对应最新柱,写条件判断时别搞反方向。RSI 周期写死 14,用 CopyBuffer(RSI_handle,0,0,rates_total,RSI) 拉值,失败就 Print 提示 bars 不够——外汇和贵金属品种在低周期可能前几十根就报这个错,属正常。 MA_handle 到 MA_handle4 分别 CopyBuffer 进 MA~MA4 四个数组,任何一处返回 <0 都打印对应失败信息。开 MT5 把这段贴进指标,改 RSIPeriod 或加 Print(ArraySize(RSI)) 能直接验证数组维度是否随品种加载量变化。
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">int RSIBuffer; class="type">int MABuffer; class="type">int RSIPeriod = class="num">14; ArrayResize(RSI, rates_total); ArrayResize(Open, rates_total); ArrayResize(Close, rates_total); CopyOpen(NULL, class="num">0, class="num">0, rates_total, Open); CopyClose(NULL, class="num">0, class="num">0, rates_total, Close); if(CopyBuffer(RSI_handle, class="num">0, class="num">0, rates_total, RSI) < class="num">0) { Print("Getting RSI values failed, not enough bars!"); } ArrayResize(MA, rates_total); if(CopyBuffer(MA_handle, class="num">0, class="num">0, rates_total, MA) < class="num">0) { Print("Getting MA values failed, not enough bars!"); } ArrayResize(MA2, rates_total); if(CopyBuffer(MA_handle2, class="num">0, class="num">0, rates_total, MA2) < class="num">0) { Print("Getting MA values failed, not enough bars!"); } ArrayResize(MA3, rates_total); if(CopyBuffer(MA_handle3, class="num">0, class="num">0, rates_total, MA3) < class="num">0) { Print("Getting MA values failed, not enough bars!"); } ArrayResize(MA4, rates_total); if(CopyBuffer(MA_handle4, class="num">0, class="num">0, rates_total, MA4) < class="num">0) { Print("Getting MA values failed, not enough bars!"); }
◍ 把高低价与均线缓冲拉进数组再算信号
指标跑之前得先把 Low、High 以及两条 MA 的缓冲区按 rates_total 长度Resize,再用 CopyLow / CopyHigh / CopyBuffer 把当前图表(NULL,0)从 0 根开始的全部数据搬进数组。若返回值小于 0,说明品种bars不够,直接 Print 报错并跳过,这是避免 MT5 上小周期加载时数组越界的最实在一步。 随后主循环从 limit-1 倒序到 0,先用 time[i] != time[i+1]+PeriodSeconds() 过滤掉非连续K线(比如跳空或手动刷新造成的重复tick),不连续就 continue。接着把 Buffer1~6 全置 EMPTY_VALUE,保证旧图形不被残留绘制。 信号写入很直接:Buffer1 买点条件是 Close>MA 且 MA>MA2 且 RSI<超卖,或 RSI<超卖且 Close>MA3,此时在 Low[i]-5*myPoint 画箭头并弹 alert 带 RSI 与 MA 两位小数。Buffer2 卖点对称,在 High[i]+5*myPoint。Buffer3 反转买是 RSI<超卖且 Close>MA,偏移放到 Low[i]-10*myPoint,比普通买点再往外扩 5 个点。外汇与贵金属波动大,这类点位仅作概率参考,实盘须自担高风险。
ArrayResize(Low, rates_total); if(CopyLow(NULL, class="num">0, class="num">0, rates_total, Low) < class="num">0) { Print("Getting LOW values failed, not enough bars!"); } ArrayResize(High, rates_total); if(CopyHigh(NULL, class="num">0, class="num">0, rates_total, High) < class="num">0) { Print("Getting HIGH values failed, not enough bars!"); } ArrayResize(MA5, rates_total); if(CopyBuffer(MA_handle5, class="num">0, class="num">0, rates_total, MA5) < class="num">0) { Print("Getting MA values failed, not enough bars!"); } ArrayResize(MA6, rates_total); if(CopyBuffer(MA_handle6, class="num">0, class="num">0, rates_total, MA6) < class="num">0) { Print("Getting MA values failed, not enough bars!"); } for(class="type">int i=limit-class="num">1; i>=class="num">0; i--) { if(i < rates_total-class="num">1 && time[i] != time[i+class="num">1]+PeriodSeconds()) { class="kw">continue; } Buffer1[i] = EMPTY_VALUE; Buffer2[i] = EMPTY_VALUE; Buffer3[i] = EMPTY_VALUE; Buffer4[i] = EMPTY_VALUE; Buffer5[i] = EMPTY_VALUE; Buffer6[i] = EMPTY_VALUE; class=class="str">"cmt">// --- Indicator calculations class=class="str">"cmt">// --- Buffer1(Buy) if((Close[i] > MA[i] && MA[i] > MA2[i] && RSI[i] < Oversold) || (RSI[i] < Oversold && Close[i] > MA3[i])) { Buffer1[i] = Low[i] - class="num">5 * myPoint; myAlert("indicator", "BUY OPPORTUNITY | RSI: " + DoubleToString(RSI[i], class="num">2) + " | MA: " + DoubleToString(MA[i], class="num">2)); } class=class="str">"cmt">// --- Buffer2(Sell) if((Close[i] < MA[i] && MA[i] < MA2[i] && RSI[i] > Overbought) || (RSI[i] > Overbought && Close[i] < MA3[i])) { Buffer2[i] = High[i] + class="num">5 * myPoint; myAlert("indicator", "SELL OPPORTUNITY | RSI: " + DoubleToString(RSI[i], class="num">2) + " | MA: " + DoubleToString(MA[i], class="num">2)); } class=class="str">"cmt">// --- Buffer3(Buy Reversal) if(RSI[i] < Oversold && Close[i] > MA[i]) { Buffer3[i] = Low[i] - class="num">10 * myPoint; myAlert("indicator", "BUY REVERSAL | RSI: " + DoubleToString(RSI[i], class="num">2) + " | MA: " + DoubleToString(MA[i], class="num">2)); } class=class="str">"cmt">// --- Buffer4(Sell Reversal)
反转与趋势信号的缓冲区落点
这段逻辑把两类信号分开画在图表缓冲区:反转类给警报,趋势类只标位置不弹窗。 RSI 突破超买且收盘价低于均线时,在 High[i] 上方 10 个点挂 Sell Reversal 箭头,并弹出带 RSI 与 MA 两位小数的警报;这里 10 * myPoint 是跨品种通用的点距写法,黄金和欧美都能直接用。 趋势方向由快慢均线 MA5 与 MA6 决定:MA5 在上方时 Buffer5 落在 Low[i] 下方 15 点,MA5 在下方时 Buffer6 落在 High[i] 上方 15 点。原作者对这两个趋势缓冲区的 myAlert 调用做了注释屏蔽,说明实盘里趋势信号频率高,弹窗容易刷屏,只留视觉标记更干净。 外汇与贵金属波动剧烈,这类信号仅作概率参考,开 MT5 把报警注释打开跑一周,就能看清自己品种上反转警报的误报密度。
if(RSI[i] > Overbought && Close[i] < MA[i]) { Buffer4[i] = High[i] + class="num">10 * myPoint; myAlert("indicator", "SELL REVERSAL | RSI: " + DoubleToString(RSI[i], class="num">2) + " | MA: " + DoubleToString(MA[i], class="num">2)); } class=class="str">"cmt">// --- Buffer5(Buy Trend) if(MA5[i] > MA6[i]) { Buffer5[i] = Low[i] - class="num">15 * myPoint; class=class="str">"cmt">//Disabled myAlert from Buffer class="num">5 class=class="str">"cmt">// myAlert("indicator", "BUY TREND | MA Fast: " + DoubleToString(MA5[i], class="num">2) + " | MA Slow: " + DoubleToString(MA6[i], class="num">2)); } class=class="str">"cmt">// --- Buffer6(Sell Trend) if(MA5[i] < MA6[i]) { Buffer6[i] = High[i] + class="num">15 * myPoint; class=class="str">"cmt">// Disabled myAlert from Buffer class="num">6 class=class="str">"cmt">// myAlert("indicator", "SELL TREND | MA Fast: " + DoubleToString(MA5[i], class="num">2) + " | MA Slow: " + DoubleToString(MA6[i], class="num">2)); } } class="kw">return(rates_total); }