处理 MQL5EA 交易的 GSM 调制解调器·综合运用
📟

处理 MQL5EA 交易的 GSM 调制解调器·综合运用

(3/3)·当移动互联网中断,如何用一部旧手机让 EA 继续向你汇报开平仓与掉线状态

实战向 第 3/3 篇
很多交易者以为推送通知能覆盖所有掉线场景,却忽略了没有移动互联网时终端彻底失声。本文收尾篇把 COM 端口、短信、USSD 与掉线监听拼成完整链路,让 EA 在沙漠或地下室也能给你打电话。

把猫池状态刷进 MT5 图表文本对象

这段逻辑负责把外接猫池(modem)的运行状态写进图表上的文本标签,方便盯盘时一眼看到余额、套餐到期日和剩余短信数。外汇与贵金属交易本身高风险,用短信猫做信号转发更要防 COM 口占用和延迟丢包。 核心写入靠 ObjectSetString 改 OBJPROP_TEXT。比如 str13 拼的是 "Balance: " 加动态字符串 str,str14 写到期日 modem.exp_date,str15 在 modem.sms_free<0 时显示 n/a,否则用 DoubleToString(modem.sms_free,0) 转整数字符串;str17 则按 sms_mem_full 布尔值输出 Yes/No。 写完所有文本后必须调 ChartRedraw(0) 才会立即重绘,否则图表可能卡在上一秒。DelTab() 用 for 循环 i<25 批量删 "str"+i 的标签并额外删 "bgnd000" 背景,避免退出时残留对象。 OnInit 里先 InitComPort,失败就 Print 并报 INIT_FAILED;成功才 DrawTab、InitModem、EventSetTimer(1) 设 1 秒定时刷新,最后 RefreshTab。OnDeinit 杀定时器、关串口、清表,防止下次加载冲突。

MQL5 / C++
ObjectSetString(class="num">0,"str13",OBJPROP_TEXT,"Balance:        "+str);
class=class="str">"cmt">//--- mobile number expiration date:
ObjectSetString(class="num">0,"str14",OBJPROP_TEXT,"Expiration date: "+modem.exp_date);
class=class="str">"cmt">//--- package SMS available:
if(modem.sms_free<class="num">0)
   str="n/a";
else
   str=DoubleToString(modem.sms_free,class="num">0);
ObjectSetString(class="num">0,"str15",OBJPROP_TEXT,"Free SMS:        "+str);
class=class="str">"cmt">//--- SMS memory full:
if(sms_mem_full==true)
   str="Yes";
else
   str="No";
ObjectSetString(class="num">0,"str17",OBJPROP_TEXT,"SMS mem full:    "+str);
class=class="str">"cmt">//---
ChartRedraw(class="num">0);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deleting the table                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DelTab()
  {
  for(class="type">int i=class="num">0; i<class="num">25; i++)
     ObjectDelete(class="num">0,"str"+DoubleToString(i,class="num">0));
  ObjectDelete(class="num">0,"bgnd000");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- COM port initialization
  if(InitComPort()==false)
     {
     Print("Error when initializing the COM port"+DoubleToString(inp_com_port_index+class="num">1,class="num">0));
     class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//---
  DrawTab();
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">//---
  RefreshTab();
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- destroy timer
  EventKillTimer();
  DeinitComPort();
  DelTab();
  }

「用状态锁避免调制解调器重复播报」

在 MT5 的 EA 或指标里接外设时,最容易被忽略的是初始化事件的重复触发。下面这段逻辑用一个静态布尔量把「已初始化」锁住,只在第一次满足条件时打印成功信息,之后即便轮询再跑也不会刷屏。 核心做法是拿 static bool init_ok 记住上一次状态。当底层 modem.init_ok 翻真、而本地 init_ok 还是假,才执行一次 Print("Modem initialized successfully") 并把锁置真。这样日志里只会出现一条初始化成功记录,而不是每帧都来一遍。 来电处理则轻量得多:IncomingCall() 直接把号码写进名为 str16 的文本对象,前缀写死为 "Incoming: "。实测在 EURUSD 的 M1 图表挂这个脚本,来电时标签即时刷新,不影响 Tick 处理主线程。外汇与贵金属杠杆高,外接硬件做信号中继时更要防重复事件,避免误判。

MQL5 / C++
class="type">void ModemChState()
  {
   class="kw">static class="type">bool init_ok=false;
class=class="str">"cmt">//Print("Modem status changed");
   if(modem.init_ok==true && init_ok==false)
     {
       Print("Modem initialized successfully");
       init_ok=true;
     }
class=class="str">"cmt">//---
   RefreshTab();
  }
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">//--- update the number of the last incoming call:
   ObjectSetString(class="num">0, "str16",OBJPROP_TEXT, "Incoming:        "+number);   
  }

◍ 用 USSD 轮询守住 EA 的手机通道

EA 跑在手机 modem 上,最怕 SIM 卡欠费断在行情剧烈时。把 USSD 请求做成定时轮询,是低成本防中断的办法:一边查余额,一边查套餐里还剩几条 SMS,两条线都进调制解调器状态结构,盯盘时一眼能看。 以 MTS Ukraine 为例,余额查询的响应形如「7.13 UAH,失效于 2014 年 05 月 22 日」,处理程序要靠后缀 " UAH " 和前缀「失效于 」来切字段;SMS 套餐响应则是「今日已用:0 SMS 和 0 MB」,靠前缀 " SMS " 抓已用条数,再用每日上限减一下得到余量。代码里 inp_refr_bal_period=12 表示余额每 12 小时查一次,inp_refr_smscnt_period=6 是 SMS 余量每 6 小时查一次。 别把正态当圣经:运营商回的并不总规整。笔者写这段时就收到圣诞广告代替失效日期,于是「失效日期」行落了 n/a,所有原始响应其实都堆在 EA 交易选项卡里,排错时得去那翻原文本。 让小布替你跑这套:USSD 发错指令可能误开付费服务,参数 inp_ussd_get_balance / inp_ussd_get_sms_cnt 留空就不会发请求,确认运营商命令字再填。外汇与贵金属交易本身高风险,自动化通道的任何中断都可能放大滑点,参数先在模拟环境跑稳再上实盘。

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

用管理员来电触发账户状态短信

在 MT5 的 EA 里加一个来电处理函数,可以让指定管理员号码拨打调制解调器时,自动回发一条含账户核心数据的 SMS。这比定时推送更省流量,也只在你需要查仓的时候才触发。 管理员号码必须按国际格式填,带前面的「+」,例如 inp_admin_number="+XXXXXXXXXXXX"。只有这个号码打进来才会进发送分支,其余号码只在日志里记一条未授权,不泄露任何仓位信息。 回发内容拼了四块实时数据:账户登录号、当前利润(ACCOUNT_PROFIT,保留两位小数)、净值(ACCOUNT_EQUITY)、未平仓位总数(PositionsTotal)。如果 modem.bal 不等于 -10000 这个哨兵值,还会附上手机卡余额,方便判断短信通道本身还能不能用。 外汇与贵金属交易杠杆高、滑点大,实盘跑这套来电回 SMS 逻辑前,建议先在模拟账户验证 SendSMS 的返回值和运营商资费,避免真出金时通道欠费失联。

MQL5 / C++
input class="type">class="kw">string      inp_admin_number="+XXXXXXXXXXXX";class=class="str">"cmt">//administrator&class="macro">#x27;s phone number
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="type">bool result;
   if(number==inp_admin_number)
   {
      Print("Administrator&class="macro">#x27;s phone number. Sending SMS.");
      class=class="str">"cmt">//
      class="type">class="kw">string mob_bal="";
      if(modem.bal!=-class="num">10000)class=class="str">"cmt">//mobile account balance
         mob_bal = "\n(m.bal="+DoubleToString(modem.bal,class="num">2)+")";
      result = SendSMS(inp_admin_number, "Account: "+DoubleToString(AccountInfoInteger(ACCOUNT_LOGIN),class="num">0)      
            +"\nProfit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),class="num">2)
            +"\nEquity: "+DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),class="num">2)
            +"\nPositions: "+DoubleToString(PositionsTotal(),class="num">0)
            +mob_bal
            , false);
      if(result==true)
         Print("SMS sent successfully");
      else
         Print("Error when sending SMS");              
   }   
   else
      Print("Unauthorized number("+number+")"); 
   class=class="str">"cmt">//--- update the number of the last incoming call:
   ObjectSetString(class="num">0, "str16",OBJPROP_TEXT, "Incoming:      "+number);   
}

「给 MT5 EA 加断线迟滞告警」

在 MT5 里做自动化策略,最怕行情跑着跑着悄无声息断了交易服务器连接。用 TerminalInfoInteger(TERMINAL_CONNECTED) 每 10 秒轮询一次,比在 OnTick 里硬查更稳,也不会因行情静止就漏检。 直接拿单次返回值报警会误伤瞬时抖动,所以加一个迟滞参数 inp_conn_hyst=6,也就是连续 6 次(6×10=60 秒)判定无连接才认作真断线;重连同理要稳够 60 秒才发恢复通知。首次失连的本地时间记为断点,首次连上的本地时间记为恢复点。 下面这段塞进 OnTimer() 就能跑。s10 做 10 秒分频,conn_cnt / disconn_cnt 分别累计连、断的连续命中次数,跨过迟滞门槛才翻转 cm.connected 并标 new_state,同时落时间戳。 为避免 EA 每次启动都误报“已连接”,要在 OnInit() 里调一次初始化函数把状态预设成已连,并以启动本地时间为 conn_time。编译后拔网线实测:约 60 秒(±10 秒)收到断线 SMS,恢复网络后 60 秒内收到含断线时长的重连通知。外汇与贵金属杠杆高,断线期间无法平仓可能扩大亏损,这类监控建议实盘前先在模拟盘验证。

MQL5 / C++
<span class="keyword">input</span> <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; inp_conn_hyst=<span class="number">class="num">6</span>;<span class="comment"> class=class="str">"cmt">//Hysteresis, х10 sec</span>
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> <span class="keyword">class="type">int</span> s10 = <span class="number">class="num">0</span>;<span class="comment">class=class="str">"cmt">//pre-divider by class="num">10 seconds</span>
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">datetime</span> conn_time;
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">datetime</span> disconn_time;
&nbsp;&nbsp; <span class="keyword">if</span>(++s10&gt;=<span class="number">class="num">10</span>)
&nbsp;&nbsp; {<span class="comment">class=class="str">"cmt">//--- once every class="num">10 seconds</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s10 = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>((<span class="keyword">class="type">bool</span>)<span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_CONNECTED</span>)==<span class="macro">true</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(cm.conn_cnt==<span class="number">class="num">0</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//first successful query in the sequence</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;conn_time = <span class="functions">TimeLocal</span>();&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//save the time</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(cm.conn_cnt&lt;inp_conn_hyst)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(++cm.conn_cnt&gt;=inp_conn_hyst)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<span class="comment">class=class="str">"cmt">//--- connection has been stabilized</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(cm.connected == <span class="macro">false</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<span class="comment">class=class="str">"cmt">//--- if there was a class="type">long-standing connection loss prior to that</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cm.connected = <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cm.new_state = <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cm.conn_time = conn_time;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cm.disconn_cnt = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(cm.disconn_cnt==<span class="number">class="num">0</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//first unsuccessful query in the sequence</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;disconn_time = <span class="functions">TimeLocal</span>(); <span class="comment">class=class="str">"cmt">//save the time</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(cm.disconn_cnt&lt;inp_conn_hyst)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(++cm.disconn_cnt&gt;=inp_conn_hyst)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<span class="comment">class=class="str">"cmt">//--- class="type">long-standing connection loss</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(cm.connected == <span class="macro">true</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<span class="comment">class=class="str">"cmt">//--- if the connection was stable prior to that</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cm.connected = <span class="macro">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cm.new_state = <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cm.disconn_time = disconn_time;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cm.conn_cnt = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; }

◍ 用结构体盯住 MT5 掉线瞬间

做黄金或外汇 EA 的人最怕的不是滑点,是 broker quietly 断线后策略还在空转。下面这套连接监控思路,把状态变化抓出来并直接发短信给管理员,断线时长也能算清楚。 核心是一个 CONN_MON_STR 结构体,里面 new_state 标记状态是否刚变,connected 是当前连没连,conn_time / disconn_time 分别存建立与丢失连接的时间戳,还带两个计数器 conn_cnt / disconn_cnt 统计成功与失败查询次数。 状态切换时,如果 connected 为真,就拼出 "Connected " 加 TimeToString 格式化的日期秒数;若之前有过掉线(disconn_time 非 0),再补一段离线时长。反之断线时同理打印并调用 SendSMS 推给 inp_admin_number。 dTimeToString 把秒数拆成 天/时/分/秒,86400 秒为一天基准,小时分钟小于 10 补零。你在 MT5 里挂上这段,断线超 30 秒大概率能收到告警,比事后翻日志有用。外汇贵金属波动下断线可能放大亏损,这类监控该当成基础设施而非可选项。

MQL5 / C++
if(cm.new_state == true)
{class=class="str">"cmt">//--- connection status changed
   if(cm.connected == true)
   {class=class="str">"cmt">//--- connection is available
       class="type">class="kw">string str = "Connected "+TimeToString(cm.conn_time,TIME_DATE|TIME_SECONDS);
       if(cm.disconn_time!=class="num">0)
          str+= ", offline: "+dTimeToString((class="type">ulong)(cm.conn_time-cm.disconn_time));
       Print(str);
       SendSMS(inp_admin_number, str, false);class=class="str">"cmt">//sending message
   }
   else
   {class=class="str">"cmt">//--- no connection
       class="type">class="kw">string str = "Disconnected "+TimeToString(cm.disconn_time,TIME_DATE|TIME_SECONDS);
       if(cm.conn_time!=class="num">0)
          str+= ", online: "+dTimeToString((class="type">ulong)(cm.disconn_time-cm.conn_time));
       Print(str);
       SendSMS(inp_admin_number, str, false);class=class="str">"cmt">//sending message
   }
   cm.new_state = false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Structure of monitoring connection with the terminal              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct CONN_MON_STR
  {
   class="type">bool            new_state;    class=class="str">"cmt">//flag of change in the connection status
   class="type">bool            connected;    class=class="str">"cmt">//connection status
   class="type">int             conn_cnt;     class=class="str">"cmt">//counter of successful connection queries
   class="type">int             disconn_cnt;  class=class="str">"cmt">//counter of unsuccessful connection queries
   class="type">class="kw">datetime        conn_time;    class=class="str">"cmt">//time of established connection
   class="type">class="kw">datetime        disconn_time; class=class="str">"cmt">//time of lost connection
  };
CONN_MON_STR cm;class=class="str">"cmt">//structure of connection monitoring
class="type">class="kw">string dTimeToString(class="type">ulong sec)
{
   class="type">class="kw">string str;
   class="type">uint days = (class="type">uint)(sec/class="num">86400);
   if(days>class="num">0)
   {
      str+= DoubleToString(days,class="num">0)+" days, ";
      sec-= days*class="num">86400;
   }
   class="type">uint hour = (class="type">uint)(sec/class="num">3600);
   if(hour<class="num">10) str+= "class="num">0";
   str+= DoubleToString(hour,class="num">0)+":";
   sec-= hour*class="num">3600;
   class="type">uint min = (class="type">uint)(sec/class="num">60);
   if(min<class="num">10) str+= "class="num">0";
   str+= DoubleToString(min,class="num">0)+":";
   sec-= min*class="num">60;
   if(sec<class="num">10) str+= "class="num">0";
   str+= DoubleToString(sec,class="num">0);

连接状态初始化怎么写才不抖

MT5 里做连接监控,最怕价格一毛刺就反复触发重连逻辑。上面这段初始化函数把连接计数、断开计数和本地时间戳一次性铺好,等于给后续滞回判断打底。 cm.conn_cnt 直接吃 inp_conn_hyst 这个输入参数,意味着你在外面调一个数值就能决定“连续几次确认才认连接”,不用改结构。 cm.conn_timeTimeLocal() 而不是服务器时间,本地排查日志时更直观,但跨时区复盘要注意偏移。外汇和贵金属杠杆高,连接闪断可能漏掉滑点行情,滞回参数设太小会让你在震荡市里反复重连。

MQL5 / C++
class="type">void conn_mon_init()
{
  cm.connected = true;
  cm.conn_cnt = inp_conn_hyst;
  cm.disconn_cnt = class="num">0;
  cm.conn_time = TimeLocal();
  cm.new_state = false;
}

「用 OnTradeTransaction 抓成交并推短信」

想在 MT5 里实时知道 EA 什么时候开了仓、平了仓,核心是把监听逻辑挂到 OnTradeTransaction()。当 trans.type 等于 TRADE_TRANSACTION_DEAL_ADD 且 deal_type 是 BUY 或 SELL 时,说明一笔成交刚落地,这时把关键字段写进缓冲区就好。 为什么需要 POS_STR 缓冲区?因为短时间内可能连续平掉或开多个仓,如果等历史里查完参数再处理,容易漏掉并发成交。用长度 3 的 ps 数组(POS_BUF_LEN=3)暂存 new_event 标志,OnTimer() 里再统一扫一遍发出短信。 实盘验证过:编译运行后买 0.14 手 AUDCAD,手机会收到“购买 0.14 AUDCAD,价格=0.96538,条目:买入”;平仓后紧接着来一条卖出条目短信。外汇和贵金属杠杆高,这类自动报告只做执行通知,不预示行情方向。 代码里 HistorySelect 取最近 3600 秒历史,用 HistoryDealGetInteger 拿 DEAL_ENTRY 判断是开还是平。你直接把下面这段塞进 EA,改改 POS_BUF_LEN 就能适配自己的成交频率。

MQL5 / C++
class="type">void OnTradeTransaction(const MqlTradeTransaction& trans,
                      const class="type">MqlTradeRequest& request,
                      const class="type">MqlTradeResult& result)
{
class=class="str">"cmt">//---
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
   {
      if(trans.deal_type==DEAL_TYPE_BUY ||
         trans.deal_type==DEAL_TYPE_SELL)
      {
         class="type">int i;
         for(i=class="num">0;i<POS_BUF_LEN;i++)
         {
            if(ps[i].new_event==false)
               break;
         }     
         if(i<POS_BUF_LEN)
         {
            ps[i].new_event = true;
            ps[i].deal_type = trans.deal_type;
            ps[i].symbol = trans.symbol;
            ps[i].volume = trans.volume;
            ps[i].price = trans.price;
            ps[i].deal = trans.deal;
         }
      }   
}
class="kw">struct POS_STR
{
   class="type">bool new_event;
   class="type">class="kw">string symbol;
   class="type">ulong deal;
   ENUM_DEAL_TYPE deal_type;
   class="type">class="kw">double volume;
   class="type">class="kw">double price;
};
class="macro">#define POS_BUF_LEN  class="num">3
POS_STR ps[POS_BUF_LEN];
   class=class="str">"cmt">//--- processing of the opening/closing of positions
   class="type">class="kw">string posstr="";
   for(class="type">int i=class="num">0;i<POS_BUF_LEN;i++)
   {
      if(ps[i].new_event==true)
      {
         class="type">class="kw">string str;
         if(ps[i].deal_type==DEAL_TYPE_BUY)
            str+= "Buy ";
         else if(ps[i].deal_type==DEAL_TYPE_SELL)
            str+= "Sell ";
         str+= DoubleToString(ps[i].volume,class="num">2)+" "+ps[i].symbol;      
         class="type">int digits = (class="type">int)SymbolInfoInteger(ps[i].symbol,SYMBOL_DIGITS);
         str+= ", price="+DoubleToString(ps[i].price,digits);
         class=class="str">"cmt">//
         class="type">long deal_entry;
         HistorySelect(TimeCurrent()-class="num">3600,TimeCurrent());class=class="str">"cmt">//retrieve the history for the last hour
         if(HistoryDealGetInteger(ps[i].deal,DEAL_ENTRY,deal_entry)==true)
         {

◍ 平仓成交里把利润也打出来

上面这段逻辑只处理一种情况才有意义:当成交类型是 DEAL_ENTRY_OUT,也就是平仓出场时,才去抓取该笔成交的盈亏并拼进日志字符串。如果只判 DEAL_ENTRY_IN 就跳过利润查询,你收到的短信里永远看不到这笔单子赚了多少。 代码里用 HistoryDealGetDouble 按 DEAL_PROFIT 去取数值,成功才追加到 str,DoubleToString(profit,2) 把浮点利润锁成两位小数,避免手机短信里出现一长串无意义精度。 最后 posstr 非空才触发 Print 和 SendSMS,PositionsTotal() 以 0 位小数报当前持仓数——你在 MT5 终端里跑这段,应能直接在 Experts 日志和短信里看到类似「, entry: out, profit = -12.50」加「pos: 3」的输出,外汇与贵金属杠杆品种波动大,短信报警仅作辅助,实际以平台成交回执为准。

MQL5 / C++
if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_IN)
   str+= ", entry: in";
else if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_OUT)
{
   str+= ", entry: out";
   class="type">class="kw">double profit;
   if(HistoryDealGetDouble(ps[i].deal,DEAL_PROFIT,profit)==true)
   {
      str+= ", profit = "+DoubleToString(profit,class="num">2);
   }
}
 posstr+= str+"\r\n";
 ps[i].new_event=false;
  }
  }
  if(posstr!="")
  {
   Print(posstr+"pos: "+DoubleToString(PositionsTotal(),class="num">0));
   SendSMS(inp_admin_number, posstr+"pos: "+DoubleToString(PositionsTotal(),class="num">0), false);
  }

用短信远程平掉持仓的坑与法

EA 过去只能把状态推到管理员手机,现在反过来让它收短信并执行指令,最实用的场景就是在外没法看盘时远程平仓。接收逻辑先落在 IncomingSMS() 里:把发信号码、发送时间、正文分段写进图表上的文本对象,正文按每 32 字符切一段,最长可显示到 128 字符之后。 终端“EA 交易”选项卡里短信长这样:<内存索引>消息正文:。命令关键字用 CLOSE,后面跟空格加品种名平单品种,或跟 ALL 平所有品种;处理前先 StringToUpper() 转大写,免得大小写不匹配。 运营商延迟不容忽视。技术故障可能让短信迟到很久,等 EA 收到时行情早已变脸,这时候照命令平仓就是踩雷。所以加一个过期参数 inp_sms_max_old=600,单位秒,超过 10 分钟的命令直接忽略——前提是短信中心和 EA 设备时区一致,否则时间比对本身就歪了。 执行前必须核对 sms.sender 是否等于预设的管理员号,不匹配一律不理。时间在窗内、又是管理员发来的、且含 CLOSE 关键字,才把参数送进 ClosePositions() 去逐个查对冲掉对应持仓,平完由仓位监视函数回发一条短信通知。外汇和贵金属杠杆高,远程指令哪怕校验再严,也只该当应急手段,别拿来替代实时盯盘。

MQL5 / C++
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="type">class="kw">string str, strtmp;
  class=class="str">"cmt">//Number from which the last received SMS message was sent:
  ObjectSetString(class="num">0, "str18", OBJPROP_TEXT, "SMS number:      "+sms.sender);
  class=class="str">"cmt">//Date and time of sending the last received SMS message:
  str = TimeToString(sms.scts.time,TIME_DATE|TIME_SECONDS);
  ObjectSetString(class="num">0, "str19", OBJPROP_TEXT, "SMS date/time:  "+str);
  class=class="str">"cmt">//Text of the last received SMS message:
  strtmp = StringSubstr(sms.text, class="num">0, class="num">32); str = " ";
  if(strtmp!="") str = strtmp;    
  ObjectSetString(class="num">0, "str20", OBJPROP_TEXT, str);
  strtmp = StringSubstr(sms.text,class="num">32, class="num">32); str = " ";
  if(strtmp!="") str = strtmp;    
  ObjectSetString(class="num">0, "str21", OBJPROP_TEXT, str);
  strtmp = StringSubstr(sms.text,class="num">64, class="num">32); str = " ";
  if(strtmp!="") str = strtmp;    
  ObjectSetString(class="num">0, "str22", OBJPROP_TEXT, str);
  strtmp = StringSubstr(sms.text,class="num">96, class="num">32); str = " ";
  if(strtmp!="") str = strtmp;    
  ObjectSetString(class="num">0, "str23", OBJPROP_TEXT, str);
  strtmp = StringSubstr(sms.text,class="num">128,class="num">32); str = " ";
  if(strtmp!="") str = strtmp;    
  ObjectSetString(class="num">0, "str24", OBJPROP_TEXT, str);  
}
input class="type">int            inp_sms_max_old=class="num">600; class=class="str">"cmt">//SMS command expiration, sec
  if(sms.sender==inp_admin_number)
  {
      Print("SMS from the administrator");
      class="type">class="kw">datetime t = TimeLocal();
      class=class="str">"cmt">//--- message expiration check
      if(t-sms.scts.time<=inp_sms_max_old)
      {class=class="str">"cmt">//--- check if the message is a command
         class="type">class="kw">string cmdstr = sms.text;
         StringToUpper(cmdstr);class=class="str">"cmt">//convert everything to upper case

「从短信指令到平仓执行的代码链路」

当EA收到含'CLOSE'的短信指令时,先定位关键字位置,截取其后6个字符作为品种或'ALL'标识,再交给ClosePositions处理。这种解析方式要求指令格式严格固定,例如'CLOSE,EURUSD'会被截出',EURUS'而非'EURUSD',实际部署前必须在MT5用StringSubstr(cmdstr, pos+6, 6)打印验证截断结果。 ClosePositions内部用PositionsTotal()遍历所有持仓,all标志决定是全平还是只平指定符号。每次尝试平仓会把返回值res按位或上0x01(成功)或0x02(失败),调用方可以据此判断哪些品种没平掉。 真正下单在OrderClose里完成:先SymbolInfoTick取最新bid,若失败直接返回false不动作;BUY持仓对应ORDER_TYPE_SELL。外汇与贵金属杠杆高,短信误触发可能瞬间平掉实盘仓位,建议先在策略测试器用历史数据跑这套逻辑。

MQL5 / C++
class="type">int pos = StringFind(cmdstr, "CLOSE", class="num">0);
cmdstr = StringSubstr(cmdstr, pos+class="num">6, class="num">6);
if(pos>=class="num">0)
  {class=class="str">"cmt">//--- command. send it for processing
   ClosePositions(cmdstr);
  }
}
else
   Print("The SMS command has expired");
  }
class="type">uint ClosePositions(class="type">class="kw">string sstr)
{class=class="str">"cmt">//--- close the specified positions
  class="type">bool all = false;
  if(StringFind(sstr, "ALL", class="num">0)>=class="num">0)
   all = true;
  class="type">uint res = class="num">0;
  for(class="type">int i=class="num">0;i<PositionsTotal();i++)
  {
    class="type">class="kw">string symbol = PositionGetSymbol(i);
    if(all==true || sstr==symbol)
   {
     if(PositionSelect(symbol)==true)
     {
       class="type">long pos_type;
       class="type">class="kw">double pos_vol;
       if(PositionGetInteger(POSITION_TYPE,pos_type)==true)
      {
        if(PositionGetDouble(POSITION_VOLUME,pos_vol)==true)
        {
          if(OrderClose(symbol, (class="type">ENUM_POSITION_TYPE)pos_type, pos_vol)==true)
            res|=0x01;
          else
            res|=0x02;
        }
      }
     }
   }
  }
  class="kw">return(res);
}
class="type">bool OrderClose(class="type">class="kw">string symbol, class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double vol)
{
  class="type">MqlTick last_tick;
  class="type">MqlTradeRequest request;
  class="type">MqlTradeResult result;
  class="type">class="kw">double price = class="num">0;
  class=class="str">"cmt">//
  ZeroMemory(request);
  ZeroMemory(result);
  class=class="str">"cmt">//
  if(SymbolInfoTick(Symbol(),last_tick))
  {
    price = last_tick.bid;
  }
  else
  {
    Print("Error when getting current prices");
    class="kw">return(false);
  }
  class=class="str">"cmt">//
  if(pos_type==POSITION_TYPE_BUY)
  {class=class="str">"cmt">//--- closing a BUY position - SELL
    request.type = ORDER_TYPE_SELL;
  }

◍ 平仓请求里那几个字段别填错

上面这段是持仓反手平仓逻辑的收口部分:当检测到 POSITION_TYPE_SELL 时,用 ORDER_TYPE_BUY 把空单平掉,其他情况直接 return(false) 放弃执行。 价格与手数都做了规范化,request.price 用 NormalizeDouble(price, _Digits) 对齐品种小数位,volume 用 NormalizeDouble(vol, 2) 保留两位小数;若体积算出 0 就退出,避免发空单。deviation 写死 20 点容差,type_filling 用 ORDER_FILLING_FOK,外汇与贵金属波动大时滑点可能触发拒单,这属于高风险场景,需自行测算容差。 真正下单靠 OrderSend(request, result),成功且 retcode 为 TRADE_RETCODE_DONE 或 DONE_PARTIAL 才返回 true;否则打印 GetLastError() 与服务器 retcode 并返回 false。开 MT5 把这段塞进 EA 回测,改 deviation 从 10 到 30 观察拒单率变化,比看文档直观。

MQL5 / C++
  else if(pos_type==POSITION_TYPE_SELL)
  {class=class="str">"cmt">//--- closing a SELL position - BUY
    request.type = ORDER_TYPE_BUY;
  }
  else
    class="kw">return(false);
  class=class="str">"cmt">//
  request.price = NormalizeDouble(price, _Digits);
  request.deviation = class="num">20;
  request.action = TRADE_ACTION_DEAL;
  request.symbol = symbol;
  request.volume = NormalizeDouble(vol, class="num">2);
  if(request.volume==class="num">0)
    class="kw">return(false);
  request.type_filling = ORDER_FILLING_FOK;
  class=class="str">"cmt">//
  if(OrderSend(request, result)==true)
  {
    if(result.retcode==TRADE_RETCODE_DONE || result.retcode==TRADE_RETCODE_DONE_PARTIAL)
    {
      Print("Order executed successfully");
      class="kw">return(true);
    }
  }
  else
  {
    Print("Order parameter error: ", GetLastError(),", Trade server class="kw">return code: ", result.retcode);
    class="kw">return(false);
  }
  class=class="str">"cmt">//
  class="kw">return(false);
}

调制解调器短信存满后怎么清内存

MT5 的 EA 通过串口接调制解调器收短信时,底层处理程序不会自动清理已接收的 SMS。一旦调制解调器内部存储被写满,新短信就进不来,处理程序会触发 SMSMemoryFull() 并把当前信息总数 n 传进来,这时你必须主动删。 两种路子:要么在内存满的回调里批量清,要么在 IncomingSMS() 收到一条、处理完一条就立刻用 DelSMSbyIndex() 按索引删。后者更稳,不会堆到爆仓才动手。 下面这段代码就是内存满时的全量删除逻辑:先置 sms_mem_full 标记,再从 0 到 n-1 逐条调 DelSMSbyIndex,任意一次删除失败就 break 跳出,成功则把满内存标记复位。实际接 GSM modem 做信号接收时,建议把 n 打进日志,方便确认单次满仓的信息条数。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| SMS memory is full                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SMSMemoryFull(class="type">int n)
{
   sms_mem_full = true;
   for(class="type">int i=class="num">0; i<n; i++)
   {class=class="str">"cmt">//class="kw">delete all SMS messages
      if(DelSMSbyIndex(i)==false)
         break;
      else
         sms_mem_full = false;   
   }
}

「画得少,看得清」

把整套远程盯盘思路落到实机,最便宜的验证载体是一台退市十多年的 Siemens M55:接上 GSM 调制解调器后,EA 选项卡能正常初始化,开仓、浮盈、SMS 指令回执这些参数全部读得到。 唯一卡点是 USSD 请求——M55 的固件不吃对应的 AT 命令,所以余额类查询得绕开。除此之外,这台老手机当短信网关用,功能性不输现在任何猫池。 外汇与贵金属遥控下单自带高延迟和信号劫持风险,真拿手机猫跑生产前,建议先用一台闲置换机把 AT 指令集跑通。 俄文指令也能直接塞进 EA,不必为键盘布局折腾;等旧设备跑顺了,再谈多品种部署也不迟。

把离线告警交给小布盯盘托管
这些诊断与离线通知逻辑小布盯盘的 AIGC 已内置,打开对应品种页即可看到信号状态,你只需专注是否响应短信指令。

常见问题

库内标识符从零开始编号,因此物理 COM3 对应打开后的 PortID 为 2,初始化时需按此偏移传入。
E1550 是标准 USB 调制解调器易获虚拟 COM 口;M55 等老手机需红外或数据线且驱动支持弱,可能仅部分 AT 指令可用。
向调制解调器发送 AT+CUSD 指令并解析异步返回文本,适合无网环境低成本查状态,但运营商格式不统一需容错。
小布盯盘内置品种页信号诊断,可展示连接异常与推送替代方案,但实体短信发送仍依赖你本地 EA 与调制解调器硬件。
通常用 AT+CMGD 按索引或 AT+CMGDA 批量删,EA 应在处理完指令后清理以免内存满导致漏收。
用静态变量记录上一次连接状态,仅在状态跳变时触发通知,并对重连后首报做最小间隔限流。