构建K线图趋势约束模型(第六部分):一体化集成·综合运用
🔧

构建K线图趋势约束模型(第六部分):一体化集成·综合运用

(3/3)· 两个相似却分头的通知程序如何合并成不冗余、带图表注释的连贯系统

进阶 第 3/3 篇
很多人把 Telegram 和 WhatsApp 通知代码各跑各的,结果重复声明、冷却期逻辑打架。合并不是复制粘贴,保留一致部分、重构差异部分才能稳住广播可靠性。

多均线句柄的初始化与失败拦截

在 MT5 自定义指标里,iMA 返回的只是一个句柄而非数值,任何一条均线创建失败都必须立刻 return(INIT_FAILED),否则后续 OnCalculate 取到空序列会直接算崩。下面这段初始化同时挂了 7 条均线:100 EMA、200 SMA、参数化快/慢 SMA、200 EMA 等,覆盖了多周期趋势过滤的常见组合。 MA_handle3 = iMA(NULL, PERIOD_CURRENT, 100, 0, MODE_EMA, PRICE_CLOSE); 这行在当前图表周期取收盘价做 100 期指数均线;若句柄小于 0,Print 打出 INVALID_HANDLE 与 GetLastError() 便于定位。MA_handle4 与 MA_handle7 分别建 200 SMA 和 200 EMA,用相同报错结构,差异只在平滑模式。 MA_handle5、MA_handle6 用外部输入 Fast_MA_period / Slow_MA_period,意味着周期不写死,用户可在属性框调参。全部句柄校验通过后 return(INIT_SUCCEEDED),指标才进入 OnCalculate 正式算值。开 MT5 把这段代码贴进新指标,改 Fast_MA_period=20、Slow_MA_period=50,编译后若日志无 INVALID_HANDLE 即说明句柄层干净。外汇与贵金属波动大,均线组合仅作概率参考,实盘须控仓。

MQL5 / C++
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);

◍ 多周期数据对齐的初始化写法

在 MT5 自定义指标里,把 M1、D1 与当前周期的价格序列拉到同一根 K 线索引上,第一步就是先给六个输出缓冲区倒序排列。ArraySetAsSeries 对 Buffer1~Buffer6 全部置 true,意味着索引 0 就是最新柱,回测或实时计算时直接按 i=0 取当前值,不用再倒算位移。 首次加载时 prev_calculated 小于 1,必须用 ArrayInitialize 把六个缓冲写满 EMPTY_VALUE,否则历史柱会残留 0 值,在副图画线时会出现从零拉起的假信号。若已计算过至少一根,则 limit 自增 1,只补算新增柱,这是降低 CPU 占用的标准做法。 跨周期对齐靠 iBarShift:用 CopyTime 取当前周期时间数组 TimeShift 并倒序,再对 M1 和 D1 分别求对应 bar 索引存进 barshift_M1、barshift_D1。rates_total 根时间全部映射完,后续读 RSI、Open(M1)、Close(D1) 就能用同一 i 定位,避免时间错位。外汇与贵金属跨周期统计存在滑点与时区跳空,实盘前应在 MT5 用 EURUSD 的 M5 加载验证一次。

MQL5 / C++
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);
 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);
 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);

「多均线句柄的缓冲拷贝与序列反转」

在 MT5 自定义指标里同时拉 7 条均线和高低价序列,第一步不是算信号,而是把每个句柄的缓冲区安全地搬进本地数组。任何一次 CopyBuffer 或 CopyLow/CopyHigh 返回小于等于 0,都直接 return,避免后面数组越界把指标跑崩。 下面这段是实际落地的拷贝骨架:先对 MA_handle 到 MA_handle4 做 BarsCalculated 守卫,再 CopyBuffer 进 MA~MA4,随后立刻 ArraySetAsSeries(..., true) 把索引翻成时间倒序——也就是 MA[0] 是最新一根 K 线。Low、High、Time 同理用 CopyLow/CopyHigh/CopyTime 拉完再反转。 主循环里有一行很容易被忽略:if (i >= MathMin(PLOT_MAXIMUM_BARS_BACK-1, rates_total-1-OMIT_OLDEST_BARS)) continue; 它的作用是跳过最老的一段 K 线。假设 PLOT_MAXIMUM_BARS_BACK 设 5000、OMIT_OLDEST_BARS 设 100,当 rates_total 足够大时,i 大于 4899 的旧柱直接跳过,既防 Array out of range,也少算无用历史。外汇和贵金属波动大,这种边界守卫能显著降低复盘卡顿概率。 把这套拷贝顺序原样塞进你的 OnCalculate,开 MT5 用 EURUSD 的 M15 跑一遍,若日志无‘array out of range’且图表均线正常跟随,说明句柄绑定没错位。

MQL5 / C++
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
  for(class="type">int i = limit-class="num">1; i >= class="num">0; i--)
     {
      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  
      

多周期条件嵌套的信号写入逻辑

这段核心循环把多周期偏移校验和多组指标条件绑在一起,只有全部通过才往对应 buffer 写值,否则填 EMPTY_VALUE 隐藏箭头。 先剔除越界样本:barshift_M1[i] 或 barshift_D1[i] 落在区间外就 continue,避免跨周期取价时读到无效索引。 买点分支要求 RSI[i] 跌破 Oversold 且前一根还在其上方(即本根完成下穿),同时 M1 的 Open 不低于 D1 前一根 Close,并且两组均线 MA>MA2、MA3>MA4 同时多头排列;满足后 Buffer1 记在 Low[1+i],若 i==1 且时间不等于已告警时间就弹 Buy。 卖点对称处理:RSI 上穿 Overbought、M1 Open 不高于 D1 前 Close、两组均线空排,Buffer2 写 High[1+i] 并可能触发 Sell 告警。 Buffer3/4 只看 MA5 与 MA6 的死叉金叉:金叉写 Low[i] 告警 Buy Reversal,死叉写 High[i],不涉及 RSI 与跨周期价。外汇与贵金属波动剧烈,这类多条件信号仅为概率倾向,实盘前请在 MT5 用历史数据验证各阈值。

MQL5 / C++
if(barshift_M1[i] < class="num">0 || barshift_M1[i] >= rates_total) class="kw">continue;
if(barshift_D1[i] < class="num">0 || barshift_D1[i] >= rates_total) class="kw">continue;

class=class="str">"cmt">//Indicator Buffer class="num">1
if(RSI[i] < Oversold
&& RSI[i+class="num">1] > Oversold class=class="str">"cmt">//Relative Strength Index crosses below fixed value
&& Open[barshift_M1[i]] >= Close[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Open >= Candlestick Close
&& MA[i] > MA2[i] class=class="str">"cmt">//Moving Average > Moving Average
&& MA3[i] > MA4[i] class=class="str">"cmt">//Moving Average > Moving Average
)
  {
   Buffer1[i] = Low[class="num">1+i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Buy"); 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(RSI[i] > Overbought
&& RSI[i+class="num">1] < Overbought class=class="str">"cmt">//Relative Strength Index crosses above fixed value
&& Open[barshift_M1[i]] <= Close[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Open <= Candlestick Close
&& MA[i] < MA2[i] class=class="str">"cmt">//Moving Average < Moving Average
&& MA3[i] < MA4[i] class=class="str">"cmt">//Moving Average < Moving Average
)
  {
   Buffer2[i] = High[class="num">1+i]; class=class="str">"cmt">//Set indicator value at Candlestick High
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Sell"); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
else
  {
   Buffer2[i] = EMPTY_VALUE;
   }
class=class="str">"cmt">//Indicator Buffer class="num">3
if(MA5[i] > MA6[i]
&& MA5[i+class="num">1] < MA6[i+class="num">1] class=class="str">"cmt">//Moving Average crosses above Moving Average
)
  {
   Buffer3[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", "Buy Reversal"); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
else
  {
   Buffer3[i] = EMPTY_VALUE;
   }
class=class="str">"cmt">//Indicator Buffer class="num">4
if(MA5[i] < MA6[i]
&& MA5[i+class="num">1] > MA6[i+class="num">1] class=class="str">"cmt">//Moving Average crosses below Moving Average
)
  {
   Buffer4[i] = High[i]; class=class="str">"cmt">//Set indicator value at Candlestick High

◍ 把反转报警改成静默画线

这段逻辑承接前面的双均线判断,核心是把原本会弹窗报警的 Buy / Sell 信号改成只往缓冲区写值、不叫唤。Buffer5 和 Buffer6 分别承载 MA3 上穿与下穿 MA7 的位置,条件满足时才赋 MA3[i],否则写 EMPTY_VALUE 让线断开。 注意原代码里 i==1 且 Time[1]!=time_alert 的 myAlert 调用全被注释掉了,time_alert 的赋值也一并注销。这意味着你在 MT5 里加载这个改法后,图表只显示箭头或线段,不会有任何声音和弹窗——适合复盘或多人共享信号时不打扰。 保留的反转报警只剩一处:i==1 且 Time[1]!=time_alert 时触发 Sell Reversal 的 myAlert,并刷新 time_alert。若你想完全静默,把这一行也注释掉即可;若想恢复买点报警,把 Buffer5 段里那两行注释放开就能用。外汇与贵金属波动剧烈,这类信号仅作参考,实际触发概率受周期与滑点影响。

MQL5 / C++
   if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Sell Reversal"); class=class="str">"cmt">//Alert on next bar open
   time_alert = Time[class="num">1];
   }
   else
     {
     Buffer4[i] = EMPTY_VALUE;
     }
   class=class="str">"cmt">//Indicator Buffer class="num">5, Alert muted by turning it into a comment
   if(MA3[i] > MA7[i] class=class="str">"cmt">//Moving Average > Moving Average
   )
     {
     Buffer5[i] = MA3[i]; class=class="str">"cmt">//Set indicator value at Moving Average
     class=class="str">"cmt">//if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Buy"); //Alert on next bar open
     class=class="str">"cmt">//time_alert = Time[class="num">1];
     }
   else
     {
     Buffer5[i] = EMPTY_VALUE;
     }
   class=class="str">"cmt">//Indicator Buffer class="num">6, Alert muted by turning it into a comment
   if(MA3[i] < MA7[i] class=class="str">"cmt">//Moving Average < Moving Average
   )
     {
     Buffer6[i] = MA3[i]; class=class="str">"cmt">//Set indicator value at Moving Average
     class=class="str">"cmt">//if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Sell"); //Alert on next bar open
     class=class="str">"cmt">//time_alert = Time[class="num">1];
     }
   else
     {
     Buffer6[i] = EMPTY_VALUE;
     }
   }
 class="kw">return(rates_total);
}

「用 Comment 把指标状态直接打在图上」

MT5 自带的 Comment 函数能在图表左上角持续刷新文本,不写日志也能肉眼看到 EA 或指标的运行节点。对外汇、贵金属这类高波动品种做自动预警时,图面实时反馈比翻 Experts 标签更省时间,但图表文本会被下一帧覆盖,只适合看“当前状态”而非留痕。 在 Trend Constraint V1.08 里,我把 Comment 塞进三个关键位置:初始化结束、警报发送成功、警报发送失败。这样加载指标后不用猜它死没死,图上直接给话。 判定发送成败用的是 result 阈值 32——大于 32 视为 Python 脚本调用成功,否则走失败分支并补一句 Failed to send message。这个 32 是案例里沿用的约定值,你接自己脚本时得按实际返回码改,别照抄。 别把图面文字当日志 Comment 不落盘,MT5 重启或图表切换后旧消息就没了。真要查某次警报为什么漏,还是得配合 Print 和 GetLastError 去日志翻,图上的字只是给你盯盘时省一步。

MQL5 / C++
class="type">int OnInit() {
    class=class="str">"cmt">// Initialization code here
    Comment("Indicator successfully launched.");
    class="kw">return INIT_SUCCEEDED;
}
if (result > class="num">32) {
    Print("Successfully executed Python script. Result code: ", result);
    Comment("Success message sent: " + message);
}
if (result <= class="num">32) {
    class="type">int error_code = GetLastError();
    Print("Failed to execute Python script. Error code: ", error_code);
    Comment("Failed to send message: " + message);
}

指标缓冲与报警冷却的底层接线

这段声明把自定义指标的绘图通道和输入参数一次性铺开:第 4、5 号缓冲画反转信号线(橙 0xFFAA00 标 Sell Reversal、蓝 0x0000FF 标 Buy),第 6 号缓冲画 Sell 线,线宽均锁 2、实线。PLOT_MAXIMUM_BARS_BACK 设 5000、OMIT_OLDEST_BARS 设 50,意味着图表只回溯 5000 根 K 线且最老 50 根不绘,避免历史太长的重绘拖累。 输入参数里 Oversold=30、Overbought=70 是 RSI 经典阈值,Slow_MA_period=200 与 Fast_MA_period=100 构成快慢均线约束。Audible_Alerts、Push_Notifications 两个布尔开关控制声音与推送,默认全开。 报警函数 myAlert 内置冷却:alert_cooldown_seconds=60,若距上次报警不足 60 秒直接 return。外汇与贵金属波动快,这种冷却能压住 MT5 弹窗轰炸,但信号本身不代表方向确定性,触发后仍需人工核对均线位。 ShellExecuteW 从 shell32.dll 导入,说明指标可能调外部程序;开 MT5 把这段直接贴进自定义指标头部,改 Oversold 到 25 看 EURUSD 15M 信号密度变化,就能验证冷却与阈值的影响。

MQL5 / C++
class="macro">#class="kw">property indicator_label4 "Sell Reversal"
class="macro">#class="kw">property indicator_type5 DRAW_LINE
class="macro">#class="kw">property indicator_style5 STYLE_SOLID
class="macro">#class="kw">property indicator_width5 class="num">2
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">08 @ " + Symbol() + "," + IntegerToString(Period()) + " | " + message;

◍ 把指标告警推到 WhatsApp 与 Telegram

在 MT5 的 EA 或指标里,告警不应只停留在终端窗口。下面这段逻辑把 type 为 indicator / info 的提示,通过 ShellExecuteW 调起本机 Python 脚本往外推,同时用 Comment() 在图表左上角回显状态。

先拼一条带品种和周期的可读串:string comment = "Alert triggered by Trend Constraint V1.08Symbol: " + Symbol() + "Period: " + IntegerToString(Period()) + "Message: " + message; 这样在日志里能直接看到是哪根周期触的警。

type 分流里,print 只进 Print(),error 会在前加 error 标签并带品种周期,order / modify 目前留空待补。真正往外发的是 indicator 和 info 分支:若 Audible_Alerts 为真就 Alert() 响铃,Push_Notifications 为真就 SendNotification() 推 MT5 移动端。 往外推 WhatsApp 时,代码写死了 Python 3.12 路径 C:\\Users\\protech\\AppData\\Local\\Programs\\Python\\Python312\\python.exe,以及脚本 send_whatsapp_message.py。用 cmd.exe /c 执行并 timeout 5 等 5 秒,ShellExecuteW 返回值 ≤32 视为失败,会 Comment("Failed to send message: " + message);成功则 Comment("Success message sent: " + message)。 别把路径当通用 这套写法硬编了 protech 用户名和 Python312 目录,换机器直接失效。开 MT5 前先改 python_path 与两个脚本路径为你本机实际值,否则只会看到 error code 而非消息送达。外汇与贵金属波动剧烈,告警延迟或丢失都可能让你错过关键价位,实盘前务必在模拟账户跑通整条链路。

MQL5 / C++
class="type">class="kw">string comment = "Alert triggered by Trend Constraint V1.class="num">08 | Symbol: " + Symbol() + " | Period: " + IntegerToString(Period()) + " | Message: " + message;
if (type == "print") {
   Print(message);
} else if (type == "error") {
   Print(type + " | Trend Constraint V1.class="num">08 @ " + Symbol() + "," + IntegerToString(Period()) + " | " + message);
} else if (type == "order") {
   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\\protech\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
   class="type">class="kw">string whatsapp_script_path = "C:\\Users\\protech\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\send_whatsapp_message.py";
   class="type">class="kw">string whatsapp_command = python_path + " \"" + whatsapp_script_path + "\" \"" + full_message + "\"";
   
   class=class="str">"cmt">// Debugging: Print the command being executed for WhatsApp
   Print("Executing command to send WhatsApp message: ", whatsapp_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_whatsapp_command = "/c " + whatsapp_command + " && timeout class="num">5";
   class="type">int whatsapp_result = ShellExecuteW(class="num">0, "open", "cmd.exe", final_whatsapp_command, NULL, class="num">0);
   if (whatsapp_result <= class="num">32) {
      class="type">int error_code = GetLastError();
      Print("Failed to execute WhatsApp Python script. Error code: ", error_code);
      Comment("Failed to send message: " + message);
   } else {
      Print("Successfully executed WhatsApp Python script. Result code: ", whatsapp_result);
      Comment("Success message sent: " + message);
   }
   class=class="str">"cmt">// Send to Telegram
   class="type">class="kw">string telegram_script_path = "C:\\Users\\protech\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\send_telegram_message.py";

「把信号推到 Telegram 并在图上留痕」

EA 在触发信号后,用 ShellExecuteW 调起 cmd 执行本地 Python 脚本把消息发到 Telegram,这条链路对外汇/贵金属自动化告警很实用,但外部进程失败不会中断 MT5 交易逻辑,属于高风险辅助环节。 拼接命令时先组合 python 路径、脚本路径和消息体,再包一层 /ctimeout 5,确保 cmd 窗口最多挂 5 秒。若 ShellExecuteW 返回值 ≤32 说明启动失败,用 GetLastError 拿错误码并通过 Comment 在图表上写“Failed to send message”提示;成功则写“Success message sent”。 初始化函数里给四个缓冲区分别绑了箭头样式:241、242、236 对应不同信号箭头,PLOT_DRAW_BEGIN 用 MathMax(Bars-PHOT_MAXIMUM_BARS_BACK+1, OMIT_OLDEST_BARS+1) 控制老数据不重绘,避免历史重算抖动。开 MT5 把这段代码塞进自己的指标 OnInit,改箭头 code 就能直接换视觉标记。

MQL5 / C++
class="type">class="kw">string telegram_command = python_path + "\"" + telegram_script_path + "\" \"" + full_message + "\"";

class=class="str">"cmt">// Debugging: Print the command being executed for Telegram
Print("Executing command to send Telegram message: ", telegram_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_telegram_command = "/c " + telegram_command + " && timeout class="num">5";
class="type">int telegram_result = ShellExecuteW(class="num">0, "open", "cmd.exe", final_telegram_command, NULL, class="num">0);
if (telegram_result <= class="num">32) {
   class="type">int error_code = GetLastError();
   Print("Failed to execute Telegram Python script. Error code: ", error_code);
   Comment("Failed to send message: " + message);
} else {
   Print("Successfully executed Telegram Python script. Result code: ", telegram_result);
   Comment("Success message sent: " + 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);
   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);

指标句柄与绘图缓冲的初始化落点

这段初始化代码把第 3 号绘图区的箭头样式锁死为 Wingdings 字符 238,同时给 4、5 号缓冲绑了空值剔除规则,避免老旧 K 线干扰信号渲染。 PlotIndexSetInteger(3, PLOT_ARROW, 238); 这行直接决定你肉眼看到的箭头形态,想换样式改这个数字即可,238 对应一个向下小三角。 RSI 用了 14 周期收盘价,三条均线分别是 7 周期 SMMA、400 周期 SMA、100 周期 EMA,任何一条句柄创建失败就打印错误并中断初始化,这在 MT5 里能立刻暴露品种或周期不支持的问题。 myPoint 对 5 位或 3 位报价乘以 10,本质是统一点值量纲,否则黄金 XAUUSD 的 3 位数和欧美 5 位数会算出偏差十倍的间距。外汇与贵金属杠杆高,参数未校验前勿上实盘。

MQL5 / C++
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)
  {
    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);

◍ 把七条均线句柄一次性挂起来

EA 或指标初始化时,常需要同时拉多条均线做共振判断。下面这段在 OnInit 里连续创建 4 条 SMA(含 200 周期固定值与 Fast/Slow 变量周期)外加 1 条 200 周期 EMA,任何一条句柄返回负值就立刻打印 INVALID_HANDLE 与 GetLastError 并回 INIT_FAILED,避免后续计算踩空指针。 MA_handle4 到 MA_handle7 的创建逻辑完全一致:iMA 第五参数 MODE_SMA 与 MODE_EMA 决定平滑方式,PRICE_CLOSE 锁定收盘价源。若你只想看 200 EMA 与快慢 SMA 交叉,可删掉 MA_handle4 那行固定 200 SMA 以省一点资源。 全部句柄拿到后 Comment 提示启动成功并返回 INIT_SUCCEEDED,MT5 终端左上角会闪一下"Indicator successfully launched."。外汇与贵金属杠杆高,句柄建失败往往意味着品种停盘或周期非法,真金白银前先开 MT5 用策略测试器跑一遍初始化日志。

MQL5 / C++
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);
  }
Comment("Indicator successfully launched.");
class="kw">return(INIT_SUCCEEDED);

「多周期时间轴与缓冲区的对齐初始化」

这段函数开头先处理数据序列方向,六个输出缓冲区和 RSI、MA 等都要设成倒序(ArraySetAsSeries 置 true),否则用 i 索引取最近一根 K 线会整反。首次加载时 prev_calculated 小于 1,必须把六个 Buffer 全用 EMPTY_VALUE 填一遍,避免历史残留值污染绘图。 接着用 CopyTime 抓当前周期全部时间戳进 TimeShift 数组,再对每一根 K 线调用 iBarShift 映射到 M1 与 D1 的 bar 索引。rates_total 若传 5000,这两个映射数组就各占 5000 个 int,实盘黄金或欧美品种在低帧周期下可能吃掉几十 KB 内存,属于可验证的占用。 RSI_handle 和 MA_handle 都要先 BarsCalculated 判空再 CopyBuffer,任何一步返回值 <=0 直接 return,防止指标句柄未就绪就写缓冲导致数组越界。CopyOpen 取 M1 开盘、CopyClose 取 D1 收盘,分别倒序排列,后续交叉周期逻辑才能用同一根索引对齐。

MQL5 / C++
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);
  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);
  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);

多均线缓冲与历史边界的同步取数

在 MT5 自定义指标里,把七条均线句柄加上高低价、时间的缓冲一次性拉满,是后续交叉判断不出 Array out of range 的前提。下面这段直接照搬了某套多周期过滤指标的取数段,核心动作是:对每个 MA_handle 先用 BarsCalculated 确认计算完成,再用 CopyBuffer 把数据塞进数组,最后 ArraySetAsSeries 置为时间倒序。 CopyLow / CopyHigh 取当前周期 PERIOD_CURRENT 的极值序列,CopyTime 拿时间轴,全部走同样的倒序处理。若任一 Copy 返回值 <=0,立即 return(rates_total) 放弃本根 K 线计算,避免脏数据进主循环。 主循环里用 PLOT_MAXIMUM_BARS_BACK 和 OMIT_OLDEST_BARS 两个宏掐掉最老的一段 K 线——比如 PLOT_MAXIMUM_BARS_BACK 设 5000、OMIT_OLDEST_BARS 设 200,则 i 大于等于 4799 的直接 continue,既能防越界也能省掉约 4% 的尾段算力。barshift_M1 / barshift_D1 负值和超界也 continue,等于先给多周期映射打了两道保险。 开 MT5 把这段贴进 OnCalculate,故意把 OMIT_OLDEST_BARS 改成 0,回看日志里 'Array out of range' 出现的概率会明显上升,就能验证边界裁剪不是可有可无。

MQL5 / C++
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
for(class="type">int i = limit-class="num">1; i >= class="num">0; i--)
  {
    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   
    
    if(barshift_M1[i] < class="num">0 || barshift_M1[i] >= rates_total) class="kw">continue;
    if(barshift_D1[i] < class="num">0 || barshift_D1[i] >= rates_total) class="kw">continue;
    
    class=class="str">"cmt">//Indicator Buffer class="num">1
    if(RSI[i] < Oversold

◍ 多周期共振的缓冲区分支写法

这段逻辑把四类信号分别写进四个指标缓冲区,买点类挂在对应 K 线低点、卖点类挂在高点,未触发时统一填 EMPTY_VALUE,避免在副图上画出无效箭头。 买条件要求 RSI 从超卖线下方上穿、M1 周期 K 线开盘价不低于前一根 D1 收盘价、两组均线 MA>MA2 与 MA3>MA4 同时成立;卖条件反之,RSI 下穿超买、开盘价不高于前 D1 收盘、两组均线空头排列。 第三、第四缓冲区只看 MA5 与 MA6 的穿越:金叉写 Low[i] 并报警 Buy Reversal,死叉写 High[i] 并报警 Sell Reversal,和前两组不同,它直接用当前柱 i 而非偏移后的 1+i。 报警都卡在 i==1 且 Time[1] 不等于 time_alert 时才触发,等于把提示锁在“下一根 Bar 开盘”这一刻,同一根 K 线不会重复弹窗。外汇与贵金属波动剧烈,这类多周期过滤只是降低噪音,信号失效概率依旧不低,上 MT5 把 Oversold/Overbought 和均线周期改成自己的参数跑一遍才知本地品种适配度。

MQL5 / C++
&& RSI[i+class="num">1] > Oversold class=class="str">"cmt">//Relative Strength Index crosses below fixed value
&& Open[barshift_M1[i]] >= Close[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Open >= Candlestick Close
&& MA[i] > MA2[i] class=class="str">"cmt">//Moving Average > Moving Average
&& MA3[i] > MA4[i] class=class="str">"cmt">//Moving Average > Moving Average
)
   {
    Buffer1[i] = Low[class="num">1+i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
    if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Buy"); 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(RSI[i] > Overbought
   && RSI[i+class="num">1] < Overbought class=class="str">"cmt">//Relative Strength Index crosses above fixed value
   && Open[barshift_M1[i]] <= Close[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Open <= Candlestick Close
   && MA[i] < MA2[i] class=class="str">"cmt">//Moving Average < Moving Average
   && MA3[i] < MA4[i] class=class="str">"cmt">//Moving Average < Moving Average
   )
    {
     Buffer2[i] = High[class="num">1+i]; class=class="str">"cmt">//Set indicator value at Candlestick High
     if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Sell"); class=class="str">"cmt">//Alert on next bar open
     time_alert = Time[class="num">1];
     }
   else
    {
     Buffer2[i] = EMPTY_VALUE;
     }
   class=class="str">"cmt">//Indicator Buffer class="num">3
   if(MA5[i] > MA6[i]
   && MA5[i+class="num">1] < MA6[i+class="num">1] class=class="str">"cmt">//Moving Average crosses above Moving Average
   )
    {
     Buffer3[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", "Buy Reversal"); class=class="str">"cmt">//Alert on next bar open
     time_alert = Time[class="num">1];
     }
   else
    {
     Buffer3[i] = EMPTY_VALUE;
     }
   class=class="str">"cmt">//Indicator Buffer class="num">4
   if(MA5[i] < MA6[i]
   && MA5[i+class="num">1] > MA6[i+class="num">1] class=class="str">"cmt">//Moving Average crosses below Moving Average
   )
    {
     Buffer4[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", "Sell Reversal"); class=class="str">"cmt">//Alert on next bar open
     time_alert = Time[class="num">1];
     }
   else
    {

「把警报静音后指标缓冲区的赋值写法」

这段逻辑出现在自定义指标的计算循环末尾,核心是把第 5、第 6 号缓冲区当成快慢均线交叉的可视化层,同时把弹窗警报整段注释掉。 当 MA3[i] 大于 MA7[i] 时,Buffer5[i] 被赋值为 MA3[i],否则写 EMPTY_VALUE;反过来 MA3[i] 小于 MA7[i] 时,Buffer6[i] 才取值 MA3[i]。这样在 MT5 数据窗口里,两条线只会在对应方向交叉的 K 线上出现数值,其余位置为空。 被注释掉的 myAlert 调用原本挂在 i==1 且 Time[1] 不等于 time_alert 的条件下,意图是下一根 bar 开盘时触发一次买卖提醒。现在直接变成注释,说明作者选择先不上警报,只留图形信号供肉眼确认。 开 MT5 把这段贴进已有的双均线指标尾部,编译后拖到 XAUUSD 的 M15 图表,能看到 Buffer5/Buffer6 仅在交叉处画点,不会弹窗——外汇与贵金属波动剧烈,仅作信号参考,实盘仍属高风险。

MQL5 / C++
      Buffer4[i] = EMPTY_VALUE;
      }
      class=class="str">"cmt">//Indicator Buffer class="num">5, Alert muted by turning it into a comment
      if(MA3[i] > MA7[i] class=class="str">"cmt">//Moving Average > Moving Average
      )
      {
       Buffer5[i] = MA3[i]; class=class="str">"cmt">//Set indicator value at Moving Average
       class=class="str">"cmt">//if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Buy"); //Alert on next bar open
       class=class="str">"cmt">//time_alert = Time[class="num">1];
      }
      else
      {
       Buffer5[i] = EMPTY_VALUE;
      }
      class=class="str">"cmt">//Indicator Buffer class="num">6, Alert muted by turning it into a comment
      if(MA3[i] < MA7[i] class=class="str">"cmt">//Moving Average < Moving Average
      )
      {
       Buffer6[i] = MA3[i]; class=class="str">"cmt">//Set indicator value at Moving Average
       class=class="str">"cmt">//if(i == class="num">1 && Time[class="num">1] != time_alert) myAlert("indicator", "Sell"); //Alert on next bar open
       class=class="str">"cmt">//time_alert = Time[class="num">1];
      }
      else
      {
       Buffer6[i] = EMPTY_VALUE;
      }
     }
  class="kw">return(rates_total);
}

把实时数据直接钉在图表上

在 MT5 里用 Comment 函数,可以把当前价格、指标读数、自定义提示直接写到图表左上角,交易者不用切窗口就能看到动态信息。对外汇和贵金属这类高波动品种,少切一次界面就可能少漏一次信号,但这类品种杠杆高、滑点大,任何信息辅助都不能消除爆仓风险。 Comment 的调用成本极低,写一行就能覆盖旧内容,适合做轻量 HUD。比如把均线拐头状态和持仓浮盈拼成字符串丢上去,比开第二个子窗口更省资源。 下面这段是 Trend Constraint V1.07 启动后在图表写注释的最小写法,实际跑起来你能立刻在左上角看到版本与状态,验证它是否随 tick 刷新。

MQL5 / C++
Comment("Trend Constraint V1.class="num">07 started");

◍ 整合之后的实时通知落点

把 Trend Constraint V1.07 与 V1.08 两个版本并进同一套程序后,代码体积从 17.46 KB 涨到 17.89 KB,增量不到 0.5 KB,却多出了 Comment 函数与多渠道告警能力。 在 MQL5 里接上警报系统后,关键事件可通过 Python 脚本外发——send_telegram_message.py 仅 0.65 KB,send_whatsapp_message.py 为 0.96 KB,轻量到能直接丢进 MT5 的 Files 目录调用。 有读者在讨论区指出,用 Socket() 或 WebRequest() 把事件甩给 telegraf 这类收集器更稳,代码更少;这条路线值得在 MT5 里开个脚本验证一下延迟。 外汇与贵金属行情波动剧烈、高风险,任何通知方案都只是辅助,实盘前请在策略测试器跑通再上真仓。

让小布替你跑这套
这些多通道信号合并与图表注释的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到信号广播状态,把重复劳动交给小布,你专注决策。

常见问题

ShellExecuteW 声明和冷却期变量应统一置顶,避免两处冗余定义导致编译冲突或冷却失效。
隐藏窗口避免干扰桌面,但无法肉眼确认广播成功;图表注释或日志打印能补上发送回执缺口。
可以,小布的品种页已把多源信号聚合并标注,不必自己写 MQL5 合并代码即可看广播状态。
保留原有冷却期变量与判断分支,在扩展发送逻辑外不删除节流代码即可。
它们作为对比基线,本篇提取相似点做合并,差异点重构进同一函数,详见系列前篇讨论。