处理 MQL5EA 交易的 GSM 调制解调器(基础篇)
用 GSM 调制解调器给 EA 发指令
在 MT5 环境里,EA 交易默认只跟终端和经纪商服务器对话,但有些场景需要把外部短信通道接进来——比如用 GSM 调制解调器让手机短信直接触发 EA 动作。这类需求在 2014 年就已有人实做并公开,原帖累计浏览 2475 次、评论 14 条,说明落地门槛不高但关注者有限。 核心思路是:EA 通过 MQL5 的 File 或 Socket 相关函数,与运行在本地/局域网的 GSM 调制解调器管理程序交换文本;调制解调器收到特定格式的短信后,把指令写进约定文件或推到端口,EA 轮询读取并解析。外汇与贵金属交易本身杠杆高、滑点大,这种外部触发链路若延迟或丢失短信,可能造成错单,实盘前务必在模拟盘跑通。 要验证可行性,直接开 MT5 新建一个 EA,用 FileOpen / FileRead 把调制解调器输出的指令文件读进来,先打印到 Experts 日志看格式对不对,再决定挂单逻辑。
◍ 断网也能用短信盯 EA
常见的远程盯盘手段,像移动终端、推送、ICQ,全都依赖互联网。一旦进了信号盲区或者移动互联网不可用,这些通道就直接废掉。本文要做的 EA,核心目标是绕开这条依赖链——通过电话或短信跟 MT5 终端保持联系,甚至在交易服务器掉线、重连时主动发通知给你。 硬件门槛很低,几乎任意 GSM 调制解调器,或带调制解调器功能的大多数手机都能扛这事。我实测选的是 Huawei E1550,它是同类中铺货最广的型号之一;后面还会拿 2003 年发布的老机器 Siemens M55 硬替调制解调器跑一遍,看会不会翻车。 外汇和贵金属本身杠杆高、滑点凶,断网漏通知可能让你在波动里裸奔,所以这条短信通道不是锦上添花,是兜底。先不急着写完整 EA,下一步先搞清楚:怎么从 EA 里把一字节数据递给调制解调器。
「EA 怎么接管虚拟 COM 口」
把调制解调器接上电脑、装好驱动之后,系统会挂出一个虚拟 COM 口,后续所有收发都走它。想在 MT5 里直接操作这个口,得先借一个叫 TrComPort.dll 的第三方库——它负责配口、查状态、读写字节,网上能同时拿到 dll 和源码。 MQL5 调用前要把结构体字段稍微改改类型以兼容。大多数外设只用 8 数据位、无校验、1 停止位,所以 EA 参数里其实只暴露「口编号」和「波特率」就够用,其余写死在库里。 初始化时 TrComPortOpen 返回的 PortID 是零基编号:COM3 对应的标识符是 2。口一开,就能跟调制解调器对话;但这套机制不限于猫——会画 K 线的老手完全可以把 EA 接到 LED 屏或滚动文本屏,把净值或某货币对实时价推上去。外汇贵金属波动剧烈,外接显示仅作辅助,实盘仍属高风险。 读之前先用 TrComPortGetQueue 看收发队列字节数,负值代表出错,错误码明细在 dll 源码文档里。缓存有数就用 TrComPortReadArray 取,长度必须跟队列返回值对齐;超时传 -1 表示用初始化时的默认毫秒数。 下发走 TrComPortWriteArray。一个简单回显例子:收到 "Hello world!" 就回 "Have a nice day!"。 最容易被忽略的是 TrComPortClose——它必须写进 EA 的 deinit。很多情况下口没关干净,重启系统前都再也打不开,拔插调制解调器也没用。
class="macro">#class="kw">import "TrComPort.dll" class="type">int TrComPortOpen(class="type">int portnum); class="type">int TrComPortClose(class="type">int portid); class="type">int TrComPortSetConfig(class="type">int portid, TrComPortParameters& parameters); class="type">int TrComPortGetConfig(class="type">int portid, TrComPortParameters& parameters); class="type">int TrComPortWriteArray(class="type">int portid, class="type">uchar& buffer[], class="type">uint length, class="type">int timeout); class="type">int TrComPortReadArray(class="type">int portid, class="type">uchar& buffer[], class="type">uint length, class="type">int timeout); class="type">int TrComPortGetQueue(class="type">int portid, class="type">uint& input_queue, class="type">uint& output_queue); class="macro">#class="kw">import class="kw">struct TrComPortParameters { class="type">uint DesiredParams; class="type">int BaudRate; class=class="str">"cmt">// Data rate class="type">int DefaultTimeout; class=class="str">"cmt">// Default timeout(in milliseconds) class="type">uchar ByteSize; class=class="str">"cmt">// Data size(class="num">4-class="num">8) class="type">uchar StopBits; class=class="str">"cmt">// Number of stop bits class="type">uchar CheckParity; class=class="str">"cmt">// Parity check(class="num">0-no,class="num">1-yes) class="type">uchar Parity; class=class="str">"cmt">// Parity type class="type">uchar RtsControl; class=class="str">"cmt">// Initial RTS state class="type">uchar DtrControl; class=class="str">"cmt">// Initial DTR state }; input ComPortList inp_com_port_index=COM3; class=class="str">"cmt">// Choosing the COM port input BaudRateList inp_com_baudrate=_9600bps; class=class="str">"cmt">// Data rate class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| COM port initialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool InitComPort() { rx_cnt=class="num">0; tx_cnt=class="num">0; tx_err=class="num">0; class=class="str">"cmt">//--- attempt to open the port PortID=TrComPortOpen(inp_com_port_index); if(PortID!=inp_com_port_index) { Print("Error when opening the COM port"+DoubleToString(inp_com_port_index+class="num">1,class="num">0)); class="kw">return(false); } else { Print("The COM port"+DoubleToString(inp_com_port_index+class="num">1,class="num">0)+" opened successfully"); class=class="str">"cmt">//--- request all parameters, so set all flags
MT5 串口读写与消息应答的落地代码
在 MT5 里接外部硬件或信号源,先得把 COM 口参数钉死。下面这段先把 DesiredParams 指名要改的字段列出来,再调 TrComPortGetConfig 读当前配置;若返回 -1 直接 return(false),说明读口失败,EA 就不会继续瞎写。 读出来后强制覆盖几个关键值:ByteSize=8(8 位数据)、Parity=0(无校验)、StopBits=0(1 个停止位)、DefaultTimeout=100(100 毫秒超时),BaudRate 用 EA 输入参数 inp_com_baudrate 动态填。改完调 TrComPortSetConfig,返回 -1 同样撤出,避免写坏端口配置。 真正收数据靠 TrComPortGetQueue 看收发缓冲字节数。rxn>0 表示接收缓冲有东西,用 TrComPortReadArray(PortID, rx_buf, rxn, -1) 一次性读进来,-1 代表不额外限超时。读到的 uchar 数组用 CharArrayToString(rx_buf,0,rxn,CP_ACP) 转成字符串,再 StringFind 找 "Hello world!"。 命中后拼回复 "Have a nice day!",StringLen 拿长度,StringToCharArray 转回 tx_buf 准备发。外汇/贵金属 EA 挂串口有断连风险,参数写错可能让终端卡死,建议先在模拟账户跑通再上真仓。
com_par.DesiredParams=tcpmpBaudRate|tcpmpDefaultTimeout|tcpmpByteSize|tcpmpStopBits|tcpmpCheckParity|tcpmpParity|tcpmpEnableRtsControl|tcpmpEnableDtrControl; class=class="str">"cmt">//--- read current parameters if(TrComPortGetConfig(PortID,com_par)==-class="num">1) class="kw">return(false);class=class="str">"cmt">//read error class=class="str">"cmt">// com_par.ByteSize=class="num">8; class=class="str">"cmt">//class="num">8 bits com_par.Parity=class="num">0; class=class="str">"cmt">//no parity check com_par.StopBits=class="num">0; class=class="str">"cmt">//class="num">1 stop bit com_par.DefaultTimeout=class="num">100; class=class="str">"cmt">//class="num">100 ms timeout com_par.BaudRate=inp_com_baudrate; class=class="str">"cmt">//rate - from the parameters of the Expert Advisor class=class="str">"cmt">//--- if(TrComPortSetConfig(PortID,com_par)==-class="num">1) class="kw">return(false);class=class="str">"cmt">//write error } class="kw">return(true); } class="type">int TrComPortGetQueue( class="type">int portid, class=class="str">"cmt">// COM port identifier class="type">uint& input_queue, class=class="str">"cmt">// Number of bytes in the input buffer class="type">uint& output_queue class=class="str">"cmt">// Number of bytes in the output buffer ); class="type">int TrComPortReadArray( class="type">int portid, class=class="str">"cmt">// Port identifier class="type">uchar& buffer[], class=class="str">"cmt">// Pointer to the buffer to read class="type">uint length, class=class="str">"cmt">// Number of data bytes class="type">int timeout class=class="str">"cmt">// Execution timeout(in ms ); class="type">int TrComPortWriteArray( class="type">int portid, class=class="str">"cmt">// Port identifier class="type">uchar& buffer[], class=class="str">"cmt">// Pointer to the initial buffer class="type">uint length, class=class="str">"cmt">// Number of data bytes class="type">int timeout class=class="str">"cmt">// Execution timeout(in ms ); class="type">uchar rx_buf[class="num">1024]; class="type">uchar tx_buf[class="num">1024]; class="type">class="kw">string rx_str; class="type">int rxn, txn; TrComPortGetQueue(PortID, rxn, txn); if(rxn>class="num">0) { class=class="str">"cmt">//--- data received in the receiving buffer class=class="str">"cmt">//--- read TrComPortReadArray(PortID, rx_buf, rxn, -class="num">1); class=class="str">"cmt">//--- convert to a class="type">class="kw">string rx_str = CharArrayToString(rx_buf,class="num">0,rxn,CP_ACP); class=class="str">"cmt">//--- check the received message(expected message "Hello world!" if(StringFind(rx_str,"Hello world!",class="num">0)!=-class="num">1) {class=class="str">"cmt">//--- if we have a match, prepare the reply class="type">class="kw">string tx_str = "Have a nice day!"; class="type">int len = StringLen(tx_str);class=class="str">"cmt">//get the length in characters class=class="str">"cmt">//--- convert to class="type">uchar buffer StringToCharArray(tx_str, tx_buf, class="num">0, len, CP_ACP);
◍ 串口写失败时的排查落点
这段 MQL5 片段只做一件事:往已打开的 COM 口发数组,发不动就打印错误。
TrComPortWriteArray(PortID, tx_buf, len, -1) 的第四个参数是超时,传 -1 表示按端口默认阻塞等待,不自行设超时。若返回值小于 0,Print("Error when writing to the port") 会把故障抛到专家日志,但不会改变缓冲区状态。
实际接 MT5 外设(如 Arduino 信号盒)时,写返回 -1 常见原因就两个:端口被其他进程占用,或 tx_buf 长度 len 超过该串口驱动单次吞吐上限(Windows 下典型为 4096 字节)。开 MT5 后先查日志里有没有这句报错,再决定重连还是拆包。
下方 TrComPortClose(int portid) 是配对用的关闭声明,释放句柄避免端口僵死,实盘前务必确认开/关成对调用。
class=class="str">"cmt">//--- send to the port if(TrComPortWriteArray(PortID, tx_buf, len, -class="num">1)<class="num">0) Print("Error when writing to the port"); } } class="type">int TrComPortClose( class="type">int portid class=class="str">"cmt">// Port identifier );
「EA 里直接调调制解调器的接口与状态结构」
在 MT5 的 EA 里接 3G/4G 调制解调器,不需要自己拼 AT 指令串。头文件已经封装好最小命令集(ATE1 开回波、AT+CSQ 取信号、AT+CMGS 发短信等),你只要从 OnInit 里按顺序调 InitComPort 和 InitModem,再在 OnTimer 里挂 ModemTimerProc 做 1 秒轮询就行。 信号质量字段 rssi 的取值很硬:0 代表 -113 dBm 或更低,31 代表 -51 dBm 或更高,99 是无数据。用 rssi_to_str() 转出来就是 '-55 dBm' 这种可读串,比裸数字直观。 状态结构 MODEM_STR 由事件处理程序填写,外部只读。比如 sim_stat 为 1 才表示卡可用,net_reg 为 1 是本网注册、5 是漫游注册;bat_charge 是 0–100 的整数,bal 初始化前写死 -10000,真实余额来自 USSD 回包。 发短信有两条暗坑:SendSMS 只能发拉丁字符、最长 158 字符,超长部分直接截断;收短信反而支持西里尔和 UCS2,但长短信不合并。闪信靠 flash=true 触发,适合发告警。 事件回调才是真正干活的地方。IncomingCall 拿到主叫号、IncomingSMS 拿到结构体、SMSMemoryFull 告诉你内存满了(n 条),此时不删旧信就不会再收新信。下面是把接口声明原样贴出来,方便你直接抄进工程。
<span class="keyword">class="type">bool</span> InitComPort(); <span class="keyword">class="type">void</span> DeinitComPort(); <span class="keyword">class="type">void</span> InitModem(); <span class="keyword">class="type">void</span> ModemTimerProc(); <span class="keyword">class="type">bool</span> ReadSMSbyIndex( <span class="keyword">class="type">int</span> index, <span class="comment">class=class="str">"cmt">// SMS message index in the modem memory</span> INCOMING_SMS_STR& sms <span class="comment">class=class="str">"cmt">// Pointer to the structure where the message will be moved</span> ); <span class="keyword">class="type">bool</span> DelSMSbyIndex( <span class="keyword">class="type">int</span> index <span class="comment">class=class="str">"cmt">// SMS message index in the modem memory</span> ); <span class="keyword">class="type">class="kw">string</span> rssi_to_str( <span class="keyword">class="type">int</span> rssi <span class="comment"> class=class="str">"cmt">// Connection quality index, values class="num">0..class="num">31, class="num">99</span> ); <span class="keyword">class="type">bool</span> SendSMS( <span class="keyword">class="type">class="kw">string</span> da, <span class="comment">class=class="str">"cmt">// Recipient&class="macro">#x27;s phone number in international format</span> <span class="keyword">class="type">class="kw">string</span> text, <span class="comment">class=class="str">"cmt">// Text of the message, Latin characters and numbers, maximum length - class="num">158 characters</span> <span class="keyword">class="type">bool</span> flash <span class="comment">class=class="str">"cmt">// Flash message flag</span> ); <span class="keyword">class="type">void</span> ModemChState(); <span class="keyword">class="type">void</span> IncomingCall( <span class="keyword">class="type">class="kw">string</span> number <span class="comment">class=class="str">"cmt">// Caller number</span> ); <span class="keyword">class="type">void</span> IncomingSMS( INCOMING_SMS_STR& sms <span class="comment">class=class="str">"cmt">// SMS message structure</span> ); <span class="keyword">class="type">void</span> SMSMemoryFull( <span class="keyword">class="type">int</span> n <span class="comment"> class=class="str">"cmt">// Number of SMS messages in the modem memory</span> ); <span class="keyword">class="kw">struct</span> MODEM_STR { <span class="keyword">class="type">bool</span> init_ok; <span class="comment">class=class="str">"cmt">// The required minimum initialized</span> <span class="comment">class=class="str">"cmt">//</span> <span class="keyword">class="type">class="kw">string</span> manufacturer; <span class="comment">class=class="str">"cmt">// Manufacturer</span> <span class="keyword">class="type">class="kw">string</span> device; <span class="comment">class=class="str">"cmt">// Model</span>