从头开始开发智能交易系统(第 17 部分):访问 web 上的数据(III)(基础篇)
「用 MQL5 抓取网页 JSON 的底层套路」
在 MT5 里做策略外数据增强,绕不开从 web 拉取 JSON。MQL5 自 build 2875 起内置了 WebRequest,但默认禁止外部域名,需要在 EA 设置的「允许 WebRequest 的 URL 列表」里手动加白,否则运行时直接返回 520 错误码。 下面这段演示了如何向一个公开汇率接口发 GET 请求并读出字段。注意 User-Agent 头不能省,部分网关会拦截空 UA 导致 403。 实际跑下来,本地回测环境若勾选了「离线模式」会令 WebRequest 失效;实盘或实时模拟盘才真能出数。一次同步耗时通常在 80–300 ms,高频 tick 内调用会明显拖慢执行,建议放在 OnTimer 而非 OnTick。
class="type">class="kw">string headers=""; class="type">class="kw">string result; class="type">char data[]; class="type">int timeout=class="num">5000; class="type">class="kw">string url="https:class=class="str">"cmt">//api.exchangerate.host/latest?base=USD"; if(WebRequest("GET",url,headers,timeout,data,result)==class="num">200) { Print("HTTP class="num">200, 返回长度=",ArraySize(data)); } else { Print("请求失败, 错误码=",GetLastError()); }
◍ 三种 web 数据通道的取舍
上篇里我们实打了三种把 web 数据喂给 EA 的路子,各有硬伤。第一种让 EA 直接抓远程页面,服务器一慢 EA 就卡在等待响应上,实盘里这种阻塞可能直接漏掉信号。 第二种用脚本当服务器、EA 当客户端、对象当信道,不阻塞是真好用,但一切时间帧就崩——你换周期对象重连的麻烦够喝一壶。 第三种拿服务配合全局变量,时间帧问题没了,却撞上 MT5 全局变量只认 double 这条铁律。不少人图省事直接塞文本进通道,本质是在绕系统设计的本意。 本篇要做的,是绕开 double 限制另写一套替代系统,但先说透:这不是点几下就能跑的活,得自己撸不少代码。
用双精度变量塞进 8 字节通道
MT5 的全局变量通道只认 double 类型,而 double 在底层是 8 字节的 QWORD。很多交易者忘了计算机按字节寻址:1 字节 = 8 位,布尔型仅占 1 位,所以一个字节里能挤下 8 个 bool。把字节拼起来,2 字节叫 WORD,4 字节叫 DWORD,8 字节叫 QWORD——这套命名来自汇编,MQL5 的有符号整数和浮点都落在这条谱系上。 具体映射很直接:char 占 1 字节,short 占 2 字节(WORD),int 占 4 字节(DWORD),long 和 double 都占 8 字节(QWORD)。float 也是 4 字节 DWORD。也就是说,一个 double 全局变量最多能原样容纳 8 个可打印字符——这是 EA 与本地服务之间做极低延迟通信的入口。 实际用法上,若消息不超过 8 个可打印字符,单次写一个全局变量即可完成传递;超过就要拆块,用多个全局变量在一个 tick 内并发写出,接收端再拼回原文。外汇与贵金属市场高杠杆、滑点频繁,这类通道只适合单机低延迟辅助,别拿它当跨网络可靠总线。 真要传长消息,可以参考 TCP/IP 或 UDP 的分段重传思路,在全局变量层自己建模“块序号 + 等待下一帧”的协议。不必从零造轮子,理解协议逻辑后,难点就只剩 MQL5 代码实现本身。
「用 union 在全局变量里塞进 8 字节信道」
MT5 的终端全局变量只能收发 double 类型,物理上固定占 8 字节。想让 EA 和服务之间传自定义字节流,直接声明 double 会丢掉字符布局的灵活性,这里改用 union 共享同一段存储:double value 和 char Info[sizeof(double)] 重叠,写字符实际落在双精度的 8 字节内部。 头文件里用 sizeof(double) 取长度,而不是硬写 8。双精度在现行平台就是 8 字节,若将来加长,这段声明会自动适配,不用手动改数组大小。 服务端是 service 程序,在 OnStart 里跑 while 循环不断写全局变量;客户端 EA 用 EventSetTimer(1) 每秒触发 OnTimer,检查 InnerChannel 是否存在,存在就 GlobalVariableGet 读进 loc.value,再用 CharArrayToString(loc.Info, 0, sizeof(uDataServer)) 把字节翻成字符串打印。注意 MQL5 的 string 沿 C/C++ 规则,扩展字符码不能直接用,要自创编码或做数据压缩才能突破 8 字节限制。 别把正态当圣经:这套内部信道本身不复杂,但把字符塞进 double 的思路能衍生出很多非常规通信玩法,外汇与贵金属交易自带高杠杆高风险,拿去传信号前先在策略测试器里跑通再上实盘。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_GlobalNameChannel "InnerChannel" class=class="str">"cmt">//+------------------------------------------------------------------+ union uDataServer { class="type">class="kw">double value; 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">//+------------------------------------------------------------------+ 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; if (GlobalVariableCheck(def_GlobalNameChannel)) { GlobalVariableGet(def_GlobalNameChannel, loc.value); Print(CharArrayToString(loc.Info, class="num">0, class="kw">sizeof(uDataServer))); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ 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; class="type">char car = class="num">33; class="kw">while (!IsStopped()) {
◍ 用全局变量做跨实例心跳通道
这段片段演示了 EA 之间借助全局变量互通状态的一种写法:先确认名为 def_GlobalNameChannel 的全局变量存在,不存在就用 GlobalVariableTemp 临时建一个,避免后续读写报空。 循环里把 uDataServer 数组逐个字节填成递增的 ASCII 字符,car 从某初值开始,到 127 后回绕到 33,相当于生成一段 33~127 的可打印字符流,塞进 loc.Info[] 再整体写回全局变量。 每次写完调 Sleep(1000) 挂起 1 秒,意味着这条通道的心跳周期就是 1 秒级;在 MT5 里打开全局变量窗口(工具→全局变量)能看到 value 每秒被刷新一次。外汇与贵金属杠杆高,这类跨 EA 通信若用于跟单或风控,要先在模拟盘验证时序竞争。
if (!GlobalVariableCheck(def_GlobalNameChannel)) GlobalVariableTemp(def_GlobalNameChannel); 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_GlobalNameChannel, loc.value); Sleep(class="num">1000);