处理 MQL5EA 交易的 GSM 调制解调器(基础篇)
📘

处理 MQL5EA 交易的 GSM 调制解调器(基础篇)

第 1/3 篇

用 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。很多情况下口没关干净,重启系统前都再也打不开,拔插调制解调器也没用。

MQL5 / C++
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 挂串口有断连风险,参数写错可能让终端卡死,建议先在模拟账户跑通再上真仓。

MQL5 / C++
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) 是配对用的关闭声明,释放句柄避免端口僵死,实盘前务必确认开/关成对调用。

MQL5 / C++
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 条),此时不删旧信就不会再收新信。下面是把接口声明原样贴出来,方便你直接抄进工程。

MQL5 / C++
<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(
&nbsp;&nbsp; <span class="keyword">class="type">int</span> index,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// SMS message index in the modem memory</span>
&nbsp;&nbsp; INCOMING_SMS_STR&amp; sms&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Pointer to the structure where the message will be moved</span>
&nbsp;&nbsp; );
<span class="keyword">class="type">bool</span> DelSMSbyIndex(
&nbsp;&nbsp; <span class="keyword">class="type">int</span> index&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;      <span class="comment">class=class="str">"cmt">// SMS message index in the modem memory</span>
&nbsp;&nbsp; );
<span class="keyword">class="type">class="kw">string</span> rssi_to_str(
&nbsp;&nbsp; <span class="keyword">class="type">int</span> rssi&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">     class=class="str">"cmt">// Connection quality index, values class="num">0..class="num">31, class="num">99</span>
&nbsp;&nbsp; );
<span class="keyword">class="type">bool</span> SendSMS(
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> da,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Recipient&class="macro">#x27;s phone number in international format</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> text,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Text of the message, Latin characters and numbers, maximum length - class="num">158 characters</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span> flash&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Flash message flag</span>
&nbsp;&nbsp; );
<span class="keyword">class="type">void</span> ModemChState();
<span class="keyword">class="type">void</span> IncomingCall(
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> number&nbsp;&nbsp;&nbsp;&nbsp;      <span class="comment">class=class="str">"cmt">// Caller number</span>
&nbsp;&nbsp; );
<span class="keyword">class="type">void</span> IncomingSMS(
&nbsp;&nbsp; INCOMING_SMS_STR&amp; sms  <span class="comment">class=class="str">"cmt">// SMS message structure</span>
&nbsp;&nbsp; );
<span class="keyword">class="type">void</span> SMSMemoryFull(
&nbsp;&nbsp; <span class="keyword">class="type">int</span> n&nbsp;&nbsp; <span class="comment">               class=class="str">"cmt">// Number of SMS messages in the modem memory</span>
&nbsp;&nbsp; );
<span class="keyword">class="kw">struct</span> MODEM_STR
{
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp; init_ok;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// The required minimum initialized</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp; manufacturer;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Manufacturer</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp; device;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Model</span>

常见问题

给服务器接一个GSM调制解调器走串口,EA定时通过AT指令发心跳短信到手机,断网也能靠短信通道确认存活与持仓状态。
先确认虚拟串口驱动已装且端口号未被占用,EA里用CreateFile打开后检查句柄是否有效,无效就重试并写日志,别直接往下发指令。
小布可接入你的指令日志,把GSM调制解调器收发的短信内容做汇总和异常提醒,你不用自己翻串口记录也能掌握EA动态。
查波特率/校验位是否和调制解调器一致、缓冲区是否溢出、AT指令结尾是否带正确回车换行,以及设备是否处于忙状态未返回OK。
要,建议用结构体存端口句柄、上次收发时间、信号强度和待发队列,每次读写前校验状态,避免发空指令或重复发同一笔。