从头开始开发智能交易系统(第 17 部分):访问 web 上的数据(III)·进阶篇
「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 发错指令。
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 秒级。
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 里开终端全局变量窗口,能直接看到这两个键值被刷新;若数值不对,优先查分支里写死的偏移量是否和你的数据结构对齐。
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,跑起来看全局变量值变化就能验证。
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)]; };