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

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

(2/3)· 直接轮询会卡死交易线程?本篇拆解本地转发与全局变量的安全接法

案例拆解 第 2/3 篇
很多 EA 作者把网页请求直接塞进 OnTick,每秒等服务器响应,平台交易线程被拖住只是时间问题。外汇贵金属杠杆高,这类停顿可能在行情跳空时让你来不及平仓。先想清楚通信结构,再写代码。

用图表对象搭一条进程间信道

EA 想在线抓数据又不想被远程服务器响应卡住,可以借 MetaTrader 5 的对象机制做一条信道:脚本当服务端把外网值写进对象,EA 当客户端定时读对象,两边互不阻塞。 原理类似内存共享区——服务器往里写、客户端读同一块,进程间解耦但连通。下面这段头文件声明了共享对象名,并在 EA 和脚本间复用。 脚本方案有个硬伤:一切换时间帧,MT5 会强制关掉脚本(即便写了无限循环),得手动重挂;若忘了挂,EA 可能读到陈旧或错误值。临时补法是对象里记脚本最后启动时间,EA 据此判断信道是否还活着。 外汇与贵金属波动剧烈、杠杆风险高,这类信道只解决通信解耦,不替代任何风控逻辑,参数与挂脚本动作都需在 MT5 实盘前自测。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#include <Inner Channel.mqh>
class="type">int OnInit()
{
    EventSetTimer(class="num">1);   class=class="str">"cmt">// 设1秒定时器,驱动OnTimer周期执行
    class="kw">return INIT_SUCCEEDED;
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
    EventKillTimer();   class=class="str">"cmt">// 退出时杀掉定时器,避免残留
}
class="type">void OnTick()
{
}
class="type">void OnTimer()
{
    Print(GetInfoInnerChannel());   class=class="str">"cmt">// 每秒打印信道内信息,验证EA能读到
}
class=class="str">"cmt">// Canal Intra Process.mqh
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#define def_NameObjectChannel  "Inner Channel Info WEB"
class="type">void CreateInnerChannel(class="type">void)
{
    class="type">long id;
    ObjectCreate(id = ChartID(), def_NameObjectChannel, OBJ_LABEL, class="num">0, class="num">0, class="num">0);  class=class="str">"cmt">// 在当前图表建一个隐藏标签对象作为信道载体
    ObjectSetInteger(id, def_NameObjectChannel, OBJPROP_COLOR, clrNONE);  class=class="str">"cmt">// 颜色设无,前台不可见
}
class="type">void RemoveInnerChannel(class="type">void)
{

「用内通道对象抓取网页报价流」

这段脚本把外部网页数据拉进 MT5 图表对象,核心靠 Inner Channel 这个自定义对象做载体。先声明三个内联函数:删除通道对象、写文本、读文本,分别对应 ObjectDelete / ObjectSetString / ObjectGetString,图表 ID 用 ChartID() 实时绑定。 主逻辑在 OnStart:建好通道后进入 while 循环,每 200 毫秒调一次 GetDataURL 抓 tradingeconomics.com/stocks 的 INDU:IND 字段,再把返回串写进通道文本。循环靠 IsStopped() 退出,最后 RemoveInnerChannel 清理。 GetDataURL 是脏活集中地:WebRequest 用 GET 拉页,超时 100 毫秒;随后用 szTest 和 szFind 做字节级校验,偏移量写死 172783 与 173474,不匹配直接返 Bad/Failed/Error。从 iInfo 起跳过空格(0x20),一直读到回车(0x0D)截断,拼出纯净数值串。 开 MT5 把 Inner Channel.mqh 放好,直接跑这段能验证:标普期货报价会以文本形式挂在图表上,外汇与贵金属同样高风险,外网延迟或结构改动都可能让解析返 Error。

MQL5 / C++
ObjectDelete(ChartID(), def_NameObjectChannel);
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void SetInfoInnerChannel(class="type">class="kw">string szArg)
{
   ObjectSetString(ChartID(), def_NameObjectChannel, OBJPROP_TEXT, szArg);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">class="kw">string GetInfoInnerChannel(class="type">void)
{
   class="kw">return ObjectGetString(ChartID(), def_NameObjectChannel, OBJPROP_TEXT);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Inner Channel.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
   CreateInnerChannel();
   while (!IsStopped())
   {
      SetInfoInnerChannel(GetDataURL("https:class=class="str">"cmt">//tradingeconomics.com/stocks", class="num">100, "<!doctype html>", class="num">2, "INDU:IND", class="num">172783, class="num">173474, 0x0D));
      Sleep(class="num">200);
   }
   RemoveInnerChannel();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string GetDataURL(class="kw">const class="type">class="kw">string url, class="kw">const class="type">int timeout, class="kw">const class="type">class="kw">string szTest, class="type">int iTest, class="kw">const class="type">class="kw">string szFind, class="type">int iPos, class="type">int iInfo, class="type">char cLimit)
{
   class="type">class="kw">string  headers, szInfo = "";
   class="type">char    post[], charResultPage[];
   class="type">int     counter;

   if (WebRequest("GET", url, NULL, NULL, timeout, post, class="num">0, charResultPage, headers) == -class="num">1) class="kw">return "Bad";
   for (class="type">int c0 = class="num">0, c1 = StringLen(szTest); (!_StopFlag) && (c0 < c1); c0++) if (szTest[c0] != charResultPage[iTest + c0]) class="kw">return "Failed";
   for (class="type">int c0 = class="num">0, c1 = StringLen(szFind); (!_StopFlag) && (c0 < c1); c0++) if (szFind[c0] != charResultPage[iPos + c0]) class="kw">return "Error";
   for (counter = class="num">0; (!_StopFlag) && (charResultPage[counter + iInfo] == 0x20); counter++);
   for (;(!_StopFlag) && (charResultPage[counter + iInfo] != cLimit); counter++) szInfo += CharToString(charResultPage[counter + iInfo]);

   class="kw">return (_StopFlag ? "" : szInfo);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用服务绕开脚本的时间帧终止坑

脚本在切换时间帧时会被终端强制终止,这是 MT5 的机制限制。若你的逻辑需要在图表周期变动后继续存活,脚本这条路就走不通,必须换载体。 服务(Service)是终端级后台程序,不依附于具体图表,因此不会因时间帧切换而退出。代价是它拿不到图表上下文,只能靠全局资源或自定义事件与其他模块通信。 知道一种方案能跑,和知道它跑不起来时怎么办,同样重要。服务的可行性在于常驻,局限性在于隔离——要在两者之间找平衡,而不是盲目替换。 下图展示的常驻结构看似轻量,但底层涉及很少被触及的终端资源调度。后续会拆开细节,方便你在自己的 EA 或指标里复用这套常驻思路。

服务与EA通信的全局变量通道

在服务和 EA 之间建立共享通信点是个麻烦事。脚本里可以用对象,但服务无法这么做;试过外部变量,性能又达不到预期(具体瓶颈可查 MQL5 外部变量文档)。后来翻终端发现一条更轻的路:全局变量。 终端里手动加的全局变量只支持 double 类型,表面看是限制,其实 double 本质就是 8 字符的短字符串,用来在程序间传数值或短消息完全够用。这就解决了跨程序通信的第一道坎——不必靠 dll 也能建通道。 剩下的问题是程序内部能否自己建全局变量,而不是只能用终端里声明的那些。实测 MQL5 允许在 EA、脚本、指标、服务里通过代码访问和创建全局变量,所以纯 MQL5 方案可行,不用被迫写 DLL。外汇与贵金属交易本身高风险,这类底层通道只解决通信,不预示任何方向。

把信道监控交给小布
这些通信信道和全局变量的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到延迟与异常,你只管判断信号真伪。

常见问题

EA 在等待响应期间会阻塞主线程,导致 OnTick 无法及时处理报价和订单,行情剧烈波动时可能漏单或无法平仓,概率上增加滑点风险。
本地服务可立即返回空响应让 EA 继续运行,再异步补齐数据,从而降低平台被外部网络延迟卡住的可能性,适合高频轮询场景。
可以,小布盯盘内置了信道延迟与全局变量异常的检测,打开品种页就能看到,不必自己写日志解析脚本。
全局变量适合跨 EA 和服务传递轻量信号,应避免写入大块文本,且需约定命名前缀,防止不同机器人互相覆盖,倾向用服务统一写、EA 只读。