从头开始开发智能交易系统(第 17 部分):访问 web 上的数据(III)·进阶篇
📘

从头开始开发智能交易系统(第 17 部分):访问 web 上的数据(III)·进阶篇

第 2/3 篇

「8字节通道下的纸条交换」

MT5 里客户端和服务器靠「交换纸条」来同步需求:客户端先告诉服务器自己要哪类数据,服务器才去生成或捞取对应内容再回传。逻辑上不复杂,落地难点全在建模。 真正卡人的地方是传输通道——当这条通道被用来送数据时,每帧你手里只有 8 个字节可用。8 字节意味着你没法直接塞一整条报价结构体,必须自己设计压缩或分片协议,否则服务端根本不知道客户端这帧要的是 tick 还是账户状态。 对外汇、贵金属这类高波动品种做自定义通道传输时,要时刻记住这 8 字节上限是硬约束,超了就得拆包,拆包逻辑写错会直接让回传错位,实盘里属于高风险坑点。

◍ 用 union 打通 EA 与服务的通信通道

服务端代码里用三个全局变量替代原先一个,本质是为客户端(EA)和服务端之间撑出一条够宽的通道。其中两个变量实际承载了 3 条不同信息——靠的是头文件里那个 8 字节宽的 union,不同名字的字段在内存里叠在同一段空间。 下面这段服务代码就是典型用法:先声明 loc、loc1、loc2 三个 uDataServer 联合体,char car 从 33 起步。循环中若全局变量不存在就临时建好三个通道名,再把 loc.Info 逐字节填成递增值(33 到 127 循环),最后用 GlobalVariableSet 把 loc.value 推入通道。 EA 侧则反向读取:GlobalVariableGet 取回服务端写好的 loc1、loc2,Print 里用 CharArrayToString 把 loc1.Info 转成字符串,同时打印 loc2.Position[0] 和 [1]。Sleep(1000) 控制每秒跑一轮,你在 MT5 里挂上服务再跑 EA,终端大概率是每秒刷一行带字符和两个数的记录。 union 这套玩法在 MQL5 进程间通信里很实用,但通道靠全局变量名约定,改名或重跑冲突时数据可能错乱。外汇和贵金属品种上做这类自动化通信测试,仍属高风险操作,参数误写可能让 EA 发错指令。

MQL5 / C++
class="macro">#class="kw">property service
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.03"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Inner Channel.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
      uDataServer loc, loc1, loc2;
      class="type">char car = class="num">33;
      
      class="kw">while (!IsStopped())
      {
            if (!GlobalVariableCheck(def_GlobalValueInChannel))
            {
                  GlobalVariableTemp(def_GlobalValueInChannel);
                  GlobalVariableTemp(def_GlobalMaskInfo);
                  GlobalVariableTemp(def_GlobalPositionInfos);
            }
            for (class="type">char c0 = class="num">0; c0 < class="kw">sizeof(uDataServer); c0++)
            {
                  loc.Info[c0] = car;
                  car = (car >= class="num">127 ? class="num">33 : car + class="num">1);
            }
            GlobalVariableSet(def_GlobalValueInChannel, loc.value);
            GlobalVariableGet(def_GlobalMaskInfo, loc1.value);
            GlobalVariableGet(def_GlobalPositionInfos, loc2.value);
            Print(CharArrayToString(loc1.Info, class="num">0, class="kw">sizeof(uDataServer)), "  ",loc2.Position[class="num">0], "   ", loc2.Position[class="num">1]);
            Sleep(class="num">1000);
      }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
Print(CharArrayToString(loc1.Info, class="num">0, class="kw">sizeof(uDataServer)), "  ",loc2.Position[class="num">0], "   ", loc2.Position[class="num">1]);
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_GlobalValueInChannel        "Inner Channel"
class="macro">#define def_GlobalMaskInfo              "Mask Info"

用联合体偷渡全局变量传行情标识

MT5 的全局变量本质是 double 容器,想塞字符串类的标的代号得绕一下。上面这段用 union 把 double、uint 数组和 char 数组叠在同一块内存上,char 数组负责装 'INDU:IND' 这种字符标识,double 视图又能直接喂给 GlobalVariableSet,等于在同一条内部通道里既传掩码又传数值。 OnTimer 里每秒跑一次:先 SetFind 按用户输入切到道琼斯或标普,再查 def_GlobalValueInChannel 是否存在;存在就取掩码打印标识和通道值。注意 SetFind 用 static int b 记住上一次选择,只有 user01 变了才重写全局变量,避免每 tick 空刷。 实盘里这种内部通道多用于 EA 与指标分进程通信。外汇、贵金属杠杆高,这类通道只解决数据传输,不替你过滤报价跳跃,真接实时仓位前先在策略测试器跑 1.04 版逻辑确认延迟在 1 秒级。

MQL5 / C++
class="macro">#define def_GlobalPositionInfos            "Positions Infos"
class=class="str">"cmt">//+------------------------------------------------------------------+
union uDataServer
{
   class="type">class="kw">double  value;
   class="type">uint    Position[class="num">2];
   class="type">char    Info[class="kw">sizeof(class="type">class="kw">double)];
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property description "Testing internal channel\nvia terminal global variable"
class="macro">#class="kw">property version "class="num">1.04"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Inner Channel.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
enum eWhat {DOW_JONES, SP500};
class="kw">input eWhat   user01 = DOW_JONES;    class=class="str">"cmt">//Search
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   EventSetTimer(class="num">1);
   
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
   uDataServer loc;
   
   SetFind();
   if (GlobalVariableCheck(def_GlobalValueInChannel))
   {
      GlobalVariableGet(def_GlobalMaskInfo, loc.value);
      Print(CharArrayToString(loc.Info, class="num">0, class="kw">sizeof(uDataServer)), "  ", GlobalVariableGet(def_GlobalValueInChannel));
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void SetFind(class="type">void)
{
   class="kw">static class="type">int b = -class="num">1;
   uDataServer loc1, loc2;
   
   if ((GlobalVariableCheck(def_GlobalValueInChannel)) && (b != user01))
   {
      b = user01;
      class="kw">switch (user01)
      {
         case DOW_JONES  :
            StringToCharArray("INDU:IND", loc1.Info, class="num">0, class="kw">sizeof(uDataServer));
            loc2.Position[class="num">0] = class="num">172783;

「把外部品种塞进全局变量」

这段逻辑干的事很直接:按品种分支把标的标识和持仓槽位写进全局变量,供 EA 其它模块跨周期读取。 以 SP500 分支为例,StringToCharArray 把字符串 "SPX:IND" 拷进 loc1.Info 缓冲区,长度受 sizeof(uDataServer) 限制;随后 loc2.Position[0] 和 [1] 分别写入 175484 与 176156,这两个数值是该品种在共享内存结构里对应的两个数据槽偏移。 写完后用 GlobalVariableSet 把 loc1.value、loc2.value 落进 def_GlobalMaskInfo 与 def_GlobalPositionInfos。在 MT5 里开终端全局变量窗口,能直接看到这两个键值被刷新;若数值不对,优先查分支里写死的偏移量是否和你的数据结构对齐。

MQL5 / C++
loc2.Position[class="num">1] = class="num">173474;
class="kw">break;
case SP500              :
      StringToCharArray("SPX:IND", loc1.Info, class="num">0, class="kw">sizeof(uDataServer));
      loc2.Position[class="num">0] = class="num">175484;
      loc2.Position[class="num">1] = class="num">176156;
      class="kw">break;
            }
         GlobalVariableSet(def_GlobalMaskInfo, loc1.value);
         GlobalVariableSet(def_GlobalPositionInfos, loc2.value);
      }
};
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 给内部通道加上异常自检

把演示用的内部通道升级成能跑的版本,核心不是多写逻辑,而是在头文件里预埋一组‘不可能由市场生成’的短消息序列,用来区分真实报价和通道异常。原文用 BAD / FAILED / ERROR 三个不超过 8 字符的宏做标记,思路是:若从 web 服务拿到的数据匹配到这些序列,就说明连接或解析出了问题,而不是市场在动。 这里有个容易被忽略的坑:即便你定义了看似冷门的字符串,行情数据理论上仍可能撞上你的错误码。所以要尽量拉高组合空间——比如借用终端全局变量扩充分组,但原文作者刻意少用了全局变量,只留一个给错误报告用,实际工程里你可以按需要放宽。 EA 主体里 OnTimer 每秒调一次 ClientServer,服务侧一旦停摆就写回 FINISH 之类的警告串;头文件中的 union uDataServer 用 double 与 char[8] 共用内存,Info 数组正好能塞进 8 字符状态码。这样编程和后期维护时,你从全局变量里读到的不再是裸数字,而是一眼能懂的故障信号。 外汇与贵金属行情通过外部服务转发时延迟和断连概率不低,这类自检机制只能降低‘把垃圾当信号’的风险,无法消除通道本身的高风险。开 MT5 把下面头文件片段塞进 Inner Channel.mqh,跑起来看全局变量值变化就能验证。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_GlobalValueInChannel      "Inner Channel"
class="macro">#define def_GlobalMaskInfo            "Mask Info"
class="macro">#define def_GlobalPositionInfos       "Positions Infos"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MSG_FailedConnection      "BAD"
class="macro">#define def_MSG_FailedReturn          "FAILED"
class="macro">#define def_MSG_FailedMask            "ERROR"
class="macro">#define def_MSG_FinishServer          "FINISH"
class=class="str">"cmt">//+------------------------------------------------------------------+
union uDataServer
{
      class="type">class="kw">double  value;
      class="type">uint    Position[class="num">2];
      class="type">char    Info[class="kw">sizeof(class="type">class="kw">double)];
};

常见问题

用 union 把行情标识打包进全局变量,按字节对齐读写,避免类型转换截断。
给每个外部品种分配独立命名前缀的全局变量,并在写入前做存在性自检。
可以,小布能监控通道读写日志,发现字节错位或超时直接弹提醒,省去手动查 bug。
网络抖动或写满会导致字节错位,加自检能在首包就丢弃脏数据,防止 EA 误判。
本地回环通常低于 5 毫秒,跨网络视服务商而定,超过 200 毫秒建议降频发包。