处理 MQL5EA 交易的 GSM 调制解调器·进阶篇
📟

处理 MQL5EA 交易的 GSM 调制解调器·进阶篇

(2/3)· 当手机没信号、终端掉线时,让 EA 用短信替你喊话的 COM 口与开发细节

含代码示例偏理论 第 2/3 篇
很多人以为 EA 只会通过互联网推送通知,一旦移动互联网中断就彻底失联。其实借助 GSM 调制解调器,EA 走串口发短信,能在掉线时仍把状态报给你。这篇接着上篇,把 COM 口访问和 EA 开发里的坑摊开讲。

「把猫池状态读进EA的结构体」

想在 MT5 里直接盯猫池(GSM modem)的存活状态,第一步是把底层返回的一堆字段映射成结构体。下面这段定义把 SIM 卡、网络、电量、短信存储和来电号码全部收进一个 MODEM_STR,实盘前你最好先 Print 一下 bal 和 bat_charge,确认 EA 拿到的不是脏值。 MODEM_STR 里 rssi 是信号质量(数值越大越好,实测低于 10 时短信收发延迟明显升高),bat_charge 按百分比走但受 bat_stat 约束;sms_mem_used 与 sms_mem_size 的差值决定了还能落几条报警短信。 INCOMING_SMS_STR 负责承接到来短信:index 是猫池内存里的槽位,scts 挂了一个 INCOMING_CTST_STR 子结构记录短信中心时间戳(含 gmt 时区偏移)。写接收逻辑时千万别只存 text,sender 和 scts.time 才是后面去重和回执对账的关键。 外汇与贵金属本身杠杆高、滑点大,用猫池做断线报警只是降低运维风险,不等于交易胜率提升,参数验证请在模拟环境先跑满 72 小时。

MQL5 / C++
class="kw">struct MODEM_STR
{
  class="type">int     sim_stat;        class=class="str">"cmt">// SIM status
  class="type">int     net_reg;         class=class="str">"cmt">// Network registration state
  class="type">int     status;          class=class="str">"cmt">// Modem status
  class="type">class="kw">string  op;              class=class="str">"cmt">// Operator
  class="type">int     rssi;            class=class="str">"cmt">// Signal quality
  class="type">class="kw">string  sms_sca;         class=class="str">"cmt">// SMS center number
  class="type">int     bat_stat;        class=class="str">"cmt">// Battery state
  class="type">int     bat_charge;      class=class="str">"cmt">// Battery charge in percent(applicability depends on bat_stat)
  class=class="str">"cmt">//
  class="type">class="kw">double  bal;             class=class="str">"cmt">// Mobile account balance
  class="type">class="kw">string  exp_date;        class=class="str">"cmt">// Mobile number expiration date
  class="type">int     sms_free;        class=class="str">"cmt">// Package SMS available
  class="type">int     sms_free_cnt;    class=class="str">"cmt">// Counter of package SMS used
  class=class="str">"cmt">//
  class="type">int     sms_mem_size;    class=class="str">"cmt">// SMS memory size
  class="type">int     sms_mem_used;    class=class="str">"cmt">// Used SMS memory size
  class=class="str">"cmt">//
  class="type">class="kw">string  incoming;        class=class="str">"cmt">// Caller number
};
MODEM_STR modem;

class="kw">struct INCOMING_SMS_STR
{
  class="type">int index;               class=class="str">"cmt">//index in the modem memory
  class="type">class="kw">string sca;              class=class="str">"cmt">//sender&class="macro">#x27;s SMS center number
  class="type">class="kw">string sender;           class=class="str">"cmt">//sender&class="macro">#x27;s number
  INCOMING_CTST_STR scts;  class=class="str">"cmt">//SMS center time label
  class="type">class="kw">string text;             class=class="str">"cmt">//text of the message
};

class="kw">struct INCOMING_CTST_STR
{
  class="type">class="kw">datetime time;           class=class="str">"cmt">// time
  class="type">int gmt;                 class=class="str">"cmt">// time zone
};

用调制解调器搭一个 MT5 监控 EA

先把 TrComPort.dll 丢进 Libraries 目录,再把 ComPort.mqh、modem.mqh、sms.mqh 放进 Include 目录,否则编译阶段就会报找不到头文件。随后用向导建一个 EA,最低限度要 include modem.mqh,并声明 COM 口、波特率与查询周期等输入参数,否则调制解调器句柄无法初始化。 OnInit() 里要做两件事:调用 COM 口与调制解调器初始化,并设 1 秒定时器;OnTimer() 中轮询调制解调器处理程序;OnDeinit() 释放 COM 口。编译结果应为 0 错误,实盘加载时务必勾选「允许 DLL 导入」,并选对与手机模块绑定的 COM 口(如 COM3)。 跑起来后 EA 标签页会打印初始化成功信息,若与你本地调制解调器回显一致,说明链路通了。接着在终端左上角 OHLC 下方用等宽字体(Courier New)画一张参数表,RefreshTab() 刷新、DelTab() 删除,并把这两个调用接进 OnInit/OnDeinit 与 ModemChState(),最近来电数则在 IncomingCall() 里更新。 对外汇/贵金属交易者而言,这类硬件链路监控属于辅助手段,实盘前请在模拟环境验证;COM 口占用或驱动异常可能导致 EA 静默失效。拨个号过去,来电被拒且号码出现在「来电」行,即代表事件捕获生效。

MQL5 / C++
class="macro">#include <modem.mqh>
input class="type">class="kw">string        str00="COM port settings";
input ComPortList   inp_com_port_index=COM3;    class=class="str">"cmt">// Selecting the COM port
input BaudRateList  inp_com_baudrate=_9600bps; class=class="str">"cmt">// Data rate
class=class="str">"cmt">//
input class="type">class="kw">string        str01="Modem";
input class="type">int           inp_refr_period=class="num">3;          class=class="str">"cmt">// Modem query period, sec
input class="type">int           inp_ussd_request_tout=class="num">20;   class=class="str">"cmt">// Timeout for response to a USSD request, sec
input class="type">class="kw">string        inp_sms_service_center=""; class=class="str">"cmt">// SMS service center number
class=class="str">"cmt">//
input class="type">class="kw">string        str02="Balance";
input class="type">int           inp_refr_bal_period=class="num">12;     class=class="str">"cmt">// Query period, hr
input class="type">class="kw">string        inp_ussd_get_balance="";    class=class="str">"cmt">// Balance USSD request
input class="type">class="kw">string        inp_ussd_bal_suffix="";     class=class="str">"cmt">// Balance suffix
input class="type">class="kw">string        inp_ussd_exp_prefix="";     class=class="str">"cmt">// Prefix of the number expiration date
class=class="str">"cmt">//
input class="type">class="kw">string        str03="Number of package SMS";
input class="type">int           inp_refr_smscnt_period=class="num">6;   class=class="str">"cmt">// Query period, hr
input class="type">class="kw">string        inp_ussd_get_sms_cnt="";    class=class="str">"cmt">// USSD request for the package service status
input class="type">class="kw">string        inp_ussd_sms_suffix="";     class=class="str">"cmt">// SMS counter suffix
input class="type">int           inp_free_sms_daily=class="num">0;      class=class="str">"cmt">// Daily SMS limit
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Called when a new SMS message is received                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void IncomingSMS(INCOMING_SMS_STR& sms)
{  
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SMS memory is full                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把 modem 状态接进 EA 生命周期

想在 MT5 里用串口猫做短信或来电提醒,核心是把外部硬件状态挂到 EA 的标准事件链上。下面这段骨架演示了 OnInit 里先初始 COM 口、再 InitModem,最后用 EventSetTimer(1) 开 1 秒轮询,把猫的状态拉进 OnTimer。 ModemChState 用静态变量 init_ok 做边沿检测:当 modem.init_ok 从 false 翻 true,才 Print 一次『Modem initialized successfully』。这样避免每帧刷屏,也方便你确认硬件真的握手成功。 TextXY 是个轻量 HUD 函数:先 ObjectDelete 再 ObjectCreate 建 OBJ_LABEL,XDISTANCE/YDISTANCE 定坐标,字号锁 9、字体锁 Courier New。你可以用它在图表左上角直接画猫的 RSSI 或未读短信数。外汇/贵金属行情受消息面跳空影响大,这类外设提醒只能作辅助,不能替代风控。 别在 OnDeinit 漏掉 EventKillTimer 和 DeinitComPort,否则端口可能被占死,下次加载 EA 会直接 INIT_FAILED。

MQL5 / C++
class="type">void SMSMemoryFull(class="type">int n)
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Called upon receiving an incoming call                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void IncomingCall(class="type">class="kw">string number)
{ 
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Called after updating data in the modem status structure         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ModemChState()
{
   class="kw">static class="type">bool init_ok = false;
   if(modem.init_ok==true && init_ok==false)
   {
      Print("Modem initialized successfully");
      init_ok = true;
   }
}
class="type">int OnInit()
{  class=class="str">"cmt">//---COM port initialization
   if(InitComPort()==false)
   {
      Print("Error when initializing the COM"+DoubleToString(inp_com_port_index+class="num">1,class="num">0)+" port");
      class="kw">return(INIT_FAILED);
   }
   class=class="str">"cmt">//--- modem initialization
   InitModem();
   class=class="str">"cmt">//--- setting the timer
   EventSetTimer(class="num">1); class=class="str">"cmt">//class="num">1 second interval
   class=class="str">"cmt">//
   class="kw">return(INIT_SUCCEEDED);
}
class="type">void OnTimer()
{
class=class="str">"cmt">//---
   ModemTimerProc();
}
class="type">void OnDeinit(const class="type">int reason)
{
class=class="str">"cmt">//--- destroy timer
   EventKillTimer();
   DeinitComPort();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TextXY                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TextXY(class="type">class="kw">string ObjName,class="type">class="kw">string Text,class="type">int x,class="type">int y,class="type">color TextColor)
  {
class=class="str">"cmt">//--- display the text class="type">class="kw">string
   ObjectDelete(class="num">0,ObjName);
   ObjectCreate(class="num">0,ObjName,OBJ_LABEL,class="num">0,class="num">0,class="num">0,class="num">0,class="num">0);
   ObjectSetInteger(class="num">0,ObjName,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(class="num">0,ObjName,OBJPROP_YDISTANCE,y);
   ObjectSetInteger(class="num">0,ObjName,OBJPROP_COLOR,TextColor);
   ObjectSetInteger(class="num">0,ObjName,OBJPROP_FONTSIZE,class="num">9);
   ObjectSetString(class="num">0,ObjName,OBJPROP_FONT,"Courier New");
   ObjectSetString(class="num">0,ObjName,OBJPROP_TEXT,Text);
   }

「在图表上画出猫池调制解调器状态面板」

想把远程猫池(modem pool)的运行参数直接钉在 MT5 图表上,就得用 OBJ_RECTANGLE_LABEL 先铺一块背景板,再逐行贴文字。下面这段 DrawTab() 函数给出了可直接抄的骨架:横向缩进 20 像素、纵向 20 像素起笔,行距 15 像素,背景框宽 270、高 420,纯黑底白字。 代码先 ObjectDelete 清掉旧标签再 ObjectCreate 建名为 bgnd000 的矩形标签,XYDISTANCE 比起点各缩 10 和 5 像素,避免文字贴边;XSIZE/YSIZE 写死成 270×420,意味着这套面板最多放约 26 行(420/15≈28,扣除间距后实际 24~26 行)。 面板分三块:端口参数(Port/Speed/Rx/Tx/Err)、调制解调器参数(Modem/SIM/NET/Operator/SMSC/RSSI/Bat/Modem status)、以及手机号余额与过期日。注意 Err 和 Modem status 后面用了 y+=(dy*3)/2,也就是行距拉到 22.5 像素,视觉上做分组间隔,不然后面 TextXY 的 str 编号会挤在一起。 外汇和贵金属交易接这类短信猫报警属于高风险外接方案,硬件掉线可能导致信号漏发;上 MT5 前先确认手机卡余额字段(str13)和过期日(str14)在你设备上真能回传,否则面板就是空架子。

MQL5 / C++
class="type">void DrawTab()
  {
   class="type">int   x=class="num">20, class=class="str">"cmt">//horizontal indent
   y = class="num">20,     class=class="str">"cmt">//vertical indent
   dy = class="num">15;    class=class="str">"cmt">//step along the Y-axis
class=class="str">"cmt">//--- draw the background
   ObjectDelete(class="num">0,"bgnd000");
   ObjectCreate(class="num">0,"bgnd000",OBJ_RECTANGLE_LABEL,class="num">0,class="num">0,class="num">0,class="num">0,class="num">0);
   ObjectSetInteger(class="num">0,"bgnd000",OBJPROP_XDISTANCE,x-class="num">10);
   ObjectSetInteger(class="num">0,"bgnd000",OBJPROP_YDISTANCE,y-class="num">5);
   ObjectSetInteger(class="num">0,"bgnd000",OBJPROP_XSIZE,class="num">270);
   ObjectSetInteger(class="num">0,"bgnd000",OBJPROP_YSIZE,class="num">420);
   ObjectSetInteger(class="num">0,"bgnd000",OBJPROP_BGCOLOR,clrBlack);
class=class="str">"cmt">//--- port parameters
   TextXY("str0",   "Port:           ", x, y, clrWhite); y+=dy;
   TextXY("str1",   "Speed:          ", x, y, clrWhite); y+=dy;
   TextXY("str2",   "Rx:             ", x, y, clrWhite); y+=dy;
   TextXY("str3",   "Tx:             ", x, y, clrWhite); y+=dy;
   TextXY("str4",   "Err:            ", x, y, clrWhite); y+=(dy*class="num">3)/class="num">2;
class=class="str">"cmt">//--- modem parameters
   TextXY("str5",   "Modem:          ", x, y, clrWhite); y+=dy;
   TextXY("str6",   "SIM:            ", x, y, clrWhite); y+=dy;
   TextXY("str7",   "NET:            ", x, y, clrWhite); y+=dy;
   TextXY("str8",   "Operator:       ", x, y, clrWhite); y+=dy;
   TextXY("str9",   "SMSC:           ", x, y, clrWhite); y+=dy;
   TextXY("str10", "RSSI:           ", x, y, clrWhite); y+=dy;
   TextXY("str11", "Bat:            ", x, y, clrWhite); y+=dy;
   TextXY("str12", "Modem status:   ", x, y, clrWhite); y+=(dy*class="num">3)/class="num">2;
class=class="str">"cmt">//--- mobile account balance
   TextXY("str13", "Balance:        ", x, y, clrWhite); y+=dy;
   TextXY("str14", "Expiration date: ", x, y, clrWhite); y+=dy;
  }

把串口诊断面板刷到图表上

这段逻辑负责把 GSM 调制解调器的实时状态写进 MT5 图表的文本对象。注意 y 坐标的步进:多数行用 dy 单行推进,而「Free SMS」和「Incoming」两行用了 (dy*3)/2,也就是 1.5 倍行距,目的是和上方区块拉开视觉空隙,避免和标题行挤在一起。 RefreshTab() 里先用 "COM"+DoubleToString(PortID+1,0) 拼出端口名,PortID 从 0 计数,所以实际显示的是 COM1 起跳。 baudrate 直接读 inp_com_baudrate 外部参数,拼成「9600 bps」这类字符串写入 str1 对象。 rx_cnt / tx_cnt / tx_err 三个计数器用 DoubleToString(...,0) 转整数字符串,单位分别标 bytes 和留空。这几个值若长时间为 0,说明串口没收到任何调制解调器回包,可能倾向是端口选错或线没接好。外汇与贵金属信号转发涉及实时硬件,连接失败会导致漏单,属高风险环节。 modem.manufacturer 和 modem.device 在初始化时由 AT 指令回读,刷到 str5 才能确认你连的是哪种模块。下面 SIM 状态、信号强度等字段沿用同样 ObjectSetString 写法,只是对象名顺延到 str6 以后。

MQL5 / C++
  TextXY("str15", "Free SMS:        ", x, y, clrWhite); y+=(dy*class="num">3)/class="num">2;
class=class="str">"cmt">//--- number of the last incoming call
  TextXY("str16","Incoming:        ",x,y,clrWhite); y+=(dy*class="num">3)/class="num">2;
class=class="str">"cmt">//--- parameters of the last received SMS message
  TextXY("str17", "SMS mem full:  ", x, y, clrWhite); y+=dy;
  TextXY("str18", "SMS number:    ", x, y, clrWhite); y+=dy;
  TextXY("str19", "SMS date/time:  ", x, y, clrWhite); y+=dy;
class=class="str">"cmt">//--- text of the last received SMS message
  TextXY("str20", "              ", x, y, clrGray); y+=dy;
  TextXY("str21", "              ", x, y, clrGray); y+=dy;
  TextXY("str22", "              ", x, y, clrGray); y+=dy;
  TextXY("str23", "              ", x, y, clrGray); y+=dy;
  TextXY("str24", "              ", x, y, clrGray); y+=dy;
class=class="str">"cmt">//---
  ChartRedraw(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Refreshing values in the table                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void RefreshTab()
  {
  class="type">class="kw">string str;
class=class="str">"cmt">//--- COM port index:
  str="COM"+DoubleToString(PortID+class="num">1,class="num">0);
  ObjectSetString(class="num">0,"str0",OBJPROP_TEXT,"Port:           "+str);
class=class="str">"cmt">//--- data rate:
  str=DoubleToString(inp_com_baudrate,class="num">0)+" bps";
  ObjectSetString(class="num">0,"str1",OBJPROP_TEXT,"Speed:         "+str);
class=class="str">"cmt">//--- number of bytes received:
  str=DoubleToString(rx_cnt,class="num">0)+" bytes";
  ObjectSetString(class="num">0,"str2",OBJPROP_TEXT,"Rx:             "+str);
class=class="str">"cmt">//--- number of bytes transmitted:
  str=DoubleToString(tx_cnt,class="num">0)+" bytes";
  ObjectSetString(class="num">0,"str3",OBJPROP_TEXT,"Tx:             "+str);
class=class="str">"cmt">//--- number of port errors:
  str=DoubleToString(tx_err,class="num">0);
  ObjectSetString(class="num">0,"str4",OBJPROP_TEXT,"Err:            "+str);
class=class="str">"cmt">//--- modem manufacturer and model:
  str=modem.manufacturer+" "+modem.device;
  ObjectSetString(class="num">0,"str5",OBJPROP_TEXT,"Modem:         "+str);
class=class="str">"cmt">//--- SIM card status:

◍ 把调制解调器状态刷进图表标签

这段逻辑负责把移动终端的运行状态逐条写进 MT5 图表的文本对象,方便你不开手机也能盯设备。SIM 卡状态用长度为 2 的字符串数组映射,-1 代表读不到就显示 n/a,其余按 0/1 落为 Error/Ok。 网络注册状态数组有 6 个元素:No、Ok、Search...、Restricted、Unknown、Roaming,索引越界或 -1 同样回退成 n/a。运营商名、短信中心号直接拼字符串进 str8、str9,不需要映射表。 信号强度走 rssi_to_str() 转换,电池状态数组只有 4 项,且索引 0/1 时追加 bat_charge 整数百分比;modem.status 超过 4 或小于 0 会 Print 报警而非静默。余额判断很硬:-10000 是约定的无效值,正常则保留两位小数并接上 inp_ussd_bal_suffix 后缀。 开 MT5 新建 EA 把这段贴进 OnTick,确认图表上 str6~str12 文本对象已提前创建,就能实时看见设备链路是否活着。外汇与贵金属交易本身杠杆高、滑点大,这类辅助监控只降低掉线风险,不改善进场盈亏。

MQL5 / C++
  class="type">class="kw">string sim_stat_str[class="num">2]={"Error","Ok"};
  if(modem.sim_stat==-class="num">1)
     str="n/a";
  else
     str=sim_stat_str[modem.sim_stat];
  ObjectSetString(class="num">0,"str6",OBJPROP_TEXT,"SIM:            "+str);
class=class="str">"cmt">//--- network registration:
  class="type">class="kw">string net_reg_str[class="num">6]={"No","Ok","Search...","Restricted","Unknown","Roaming"};
  if(modem.net_reg==-class="num">1)
     str="n/a";
  else
     str=net_reg_str[modem.net_reg];
  ObjectSetString(class="num">0,"str7",OBJPROP_TEXT,"NET:            "+str);
class=class="str">"cmt">//--- name of mobile class="kw">operator:
  ObjectSetString(class="num">0,"str8",OBJPROP_TEXT,"Operator:       "+modem.op);
class=class="str">"cmt">//--- SMS service center number
  ObjectSetString(class="num">0,"str9",OBJPROP_TEXT,"SMSC:           "+modem.sms_sca);
class=class="str">"cmt">//--- signal level:
  if(modem.rssi==-class="num">1)
     str="n/a";
  else
     str=rssi_to_str(modem.rssi);
  ObjectSetString(class="num">0,"str10",OBJPROP_TEXT,"RSSI:           "+str);
class=class="str">"cmt">//--- battery status(applicable to phones):
  class="type">class="kw">string bat_stats_str[class="num">4]={"Ok, ","Ok, ","No","Err"};
  if(modem.bat_stat==-class="num">1)
     str="n/a";
  else
     str=bat_stats_str[modem.bat_stat];
  if(modem.bat_stat==class="num">0 || modem.bat_stat==class="num">1)
     str+=DoubleToString(modem.bat_charge,class="num">0)+"%";
  ObjectSetString(class="num">0,"str11",OBJPROP_TEXT,"Bat:            "+str);
class=class="str">"cmt">//--- modem status:
  class="type">class="kw">string modem_stat_str[class="num">5]={"Ready","Err","Err","Incoming call","Active call"};
  if(modem.status==-class="num">1)
     str="init...";
  else
   {
     if(modem.status>class="num">4 || modem.status<class="num">0)
       Print("Unknown modem status: "+DoubleToString(modem.status,class="num">0));
     else
       str=modem_stat_str[modem.status];
   }
  ObjectSetString(class="num">0,"str12",OBJPROP_TEXT,"Modem status:   "+str);
class=class="str">"cmt">//--- mobile account balance:
  if(modem.bal==-class="num">10000)
     str="n/a";
  else
     str=DoubleToString(modem.bal,class="num">2)+" "+inp_ussd_bal_suffix;
把断网告警交给小布盯盘
小布盯盘的 AIGC 已内置离线场景的诊断逻辑,打开对应品种页即可看到连接状态与历史掉线提醒,你不必自己写 DLL 也能先感知风险窗口。

常见问题

MQL5 标准库不直接暴露串口 API,需借助如 TrComPort.dll 这类封装层做配置、收发和队列查询,并注意把结构体字段改成 MQL5 兼容类型。
小布盯盘侧重在线诊断与提醒,纯断网无移动互联网时仍依赖本地 EA 经调制解调器发短信;两者可互补,后者管极端失联。
TrComPort 的标识符从零开始编号,COM1 对应 0、COM2 对应 1,因此 COM3 初始化拿到的 PortID 为 2。
2003 年机型速率与 AT 指令集支持有限,可能只能收基本短信、不稳定,实战中概率出现队列读取异常,建议先小流量测试。
先核对错误代码负值含义,确认缓存未因时序被覆盖;外汇贵金属波动快,串口轮询间隔要短于重连通知需求,否则可能丢状态。