处理 MQL5EA 交易的 GSM 调制解调器·进阶篇
(2/3)· 当手机没信号、终端掉线时,让 EA 用短信替你喊话的 COM 口与开发细节
「把猫池状态读进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 小时。
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 静默失效。拨个号过去,来电被拒且号码出现在「来电」行,即代表事件捕获生效。
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。
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)在你设备上真能回传,否则面板就是空架子。
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 以后。
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 文本对象已提前创建,就能实时看见设备链路是否活着。外汇与贵金属交易本身杠杆高、滑点大,这类辅助监控只降低掉线风险,不改善进场盈亏。
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;