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

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

第 2/2 篇

◍ 从网页源码里抠出报价数据

想在 MT5 里自动抓外部网站的数据,第一步不是写 EA,而是先学会用浏览器开发者工具看网页源码。Chrome 或其它浏览器都行,按 F12 打开开发工具,定位到包含目标数据的模块——比如股票列表里某一行的标签结构,弄清楚数据被包在哪种 HTML 节点里。 光看渲染后的页面有时不够,尤其当数据混了图片、链接等元素,字符串搜索容易误报。丢进十六进制编辑器看原始字节,能确认建模类型:是纯文本段还是嵌在复杂结构里。这一步我们暂不关心动态偏移量,重点先看清楚数据长什么样。 下面这段脚本演示了不依赖偏移、只按关键字捕捉值的思路。即使页面因偏移变动导致位置漂移,只要关键标签还在,就能捞到内容。注意循环里 Sleep(200) 代表每 200 毫秒轮一次,信息可能有轻微延迟,外汇与贵金属行情波动快、杠杆高,用这类抓取做决策前务必自行验证时效。 #property copyright "Daniel Jose" #property version "1.00" //+------------------------------------------------------------------+ void OnStart() { while (!IsStopped()) { Print(GetDataURL("https://tradingeconomics.com/stocks")); Sleep(200); } } //+------------------------------------------------------------------+ string GetDataURL(const string url, const int timeout = 100) { string headers, szInfo; char post[], charResultPage[]; int handle; if (WebRequest("GET", url, NULL, NULL, timeout, post, 0, charResultPage, headers) == -1) return "Bad return"; szInfo = ""; for (int c0 = 0, c1 = ArraySize(charResultPage); c0 < c1; c0++) szInfo += CharToString(charResultPage[c0]); if ((handle = StringFind(szInfo, "data-symbol=\"INDU:IND\"", 0)) >= 0) { handle = StringFind(szInfo, "<td id=\"p\" class=\"datatable-item\">", handle); for(; charResultPage[handle] != 0x0A; handle++); for(handle++; charResultPage[handle] != 0x0A; handle++); szInfo = ""; for(handle++; charResultPage[handle] == 0x20; handle++); for(; (charResultPage[handle] != 0x0D) && (charResultPage[handle] != 0x20); handle++) szInfo += CharToString(charResultPage[handle]); } return szInfo; } 代码逐行拆解:#property 两行是脚本元信息,版权和版本号。OnStart 是 MT5 脚本入口,while(!IsStopped()) 让脚本持续运行直到用户停止,里面每轮调用 GetDataURL 抓地址并打印,Sleep(200) 控节奏。GetDataURL 函数先用 WebRequest 发 GET 请求,超时 100 毫秒,失败返回 "Bad return"。随后把返回字节数组转成字符串 szInfo。StringFind 找 "data-symbol=\"INDU:IND\"" 定位道琼斯标的行,再找具体 td 节点;接着用 0x0A(换行)、0x20(空格)、0x0D(回车)等字节值跳过标签和空白,最终抠出净数值返回。 别把正则当万能 上述脚本对静态结构页面够用,但动态内容下循环找字节边界有点笨重。若页面设计不变、只是数据刷新,用十六进制拿到偏移后直接跳地址会省计算时间。自己把代码贴进 MT5 脚本跑一遍,看 Print 出来的延迟和数值,比读文字直观得多。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
    class="kw">while (!IsStopped())
    {
        Print(GetDataURL("https:class=class="str">"cmt">//tradingeconomics.com/stocks"));
        Sleep(class="num">200);
    }
}
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="num">100)
{
    class="type">class="kw">string  headers, szInfo;
    class="type">char    post[], charResultPage[];
    class="type">int     handle;

    if (WebRequest("GET", url, NULL, NULL, timeout, post, class="num">0, charResultPage, headers) == -class="num">1)
        class="kw">return "Bad class="kw">return";

    szInfo = "";
    for (class="type">int c0 = class="num">0, c1 = ArraySize(charResultPage); c0 < c1; c0++) szInfo += CharToString(charResultPage[c0]);
    if ((handle = StringFind(szInfo, "data-symbol=\"INDU:IND\"", class="num">0)) >= class="num">0)
    {
        handle = StringFind(szInfo, "<td id=\"p\" class=\"datatable-item\">", handle);
        for(; charResultPage[handle] != 0x0A; handle++);
        for(handle++; charResultPage[handle] != 0x0A; handle++);
        szInfo = "";
        for(handle++; charResultPage[handle] == 0x20; handle++);
        for(; (charResultPage[handle] != 0x0D) && (charResultPage[handle] != 0x20); handle++) szInfo += CharToString(charResultPage[handle]);
    }
    class="kw">return szInfo;
}

「用 MT5 抓取网页行情字段」

想在 MT5 里直接读外部网页的行情文本,可以走 WebRequest 这条路。下面这段脚本在 OnStart 里死循环调用 GetDataURL,每 200 毫秒向 tradingeconomics.com/stocks 发一次 GET,尝试抠出 IND 代码对应的数值;循环靠 IsStopped 退出,手动终止脚本才会停。 GetDataURL 先以 WebRequest 拉整页字节流,超时设 100 毫秒。若返回 -1 直接吐“Bad return”,否则用 szFind(本例 'INDU:IND')做锚点,从 iPos=172783 偏移处逐字符比对,不匹配就报“Error in Position”——这两个魔数偏移是页面结构相关的,换网址必须重抓源码算位置。 匹配通过后,从 iInfo=173474 起跳过 0x20 空格,一直读到分隔符 cLimit=0x0D(回车)为止,拼成字符串返回。实盘前先在策略测试器外跑一遍,确认目标站未改版;外汇与贵金属本身高杠杆高风险,这类外源数据仅作辅助,不替代报价窗口。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
      class="kw">while (!IsStopped())
      {
          Print(GetDataURL("https:class=class="str">"cmt">//tradingeconomics.com/stocks", class="num">100, "INDU:IND", class="num">172783, class="num">173474, 0x0D));
          Sleep(class="num">200);
      }
}
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 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 class="kw">return";
      for (class="type">int c0 = class="num">0, c1 = StringLen(szFind); c0 < c1; c0++) if (szFind[c0] != charResultPage[iPos + c0]) class="kw">return "Error in Position";
      for (counter = class="num">0; charResultPage[counter + iInfo] == 0x20; counter++);
      for (;charResultPage[counter + iInfo] != cLimit; counter++) szInfo += CharToString(charResultPage[counter + iInfo]);

      class="kw">return szInfo;
}
for (class="type">int c0 = class="num">0, c1 = StringLen(szFind); c0 < c1; c0++) if (szFind[c0] != charResultPage[iPos + c0]) class="kw">return "Error in Position";

服务器回包异常时的偏移校验兜底

WebRequest 即便在 MT5 端返回成功(不等于 -1),服务器仍可能吐回非预期报文。此时平台层不报错,但下游若直接解析就会吃到虚拟数据或内存垃圾,外汇与贵金属行情抓取尤其怕这种静默污染。 解决办法是在原偏移脚本里加一道字节级比对:用预设标签(如 "<!doctype html>")按偏移量 iTest 逐个字符核对返回缓冲。高亮的那段 for 循环就是这道闸——当报文结构比预期更复杂时,前面首检过了再跑它,能在分析数据上留足安全裕度。原文实测改动后输出无差异,说明系统仍按预期工作,但这种罕见情况发生概率不应低估。 眼下我们只是把网页数据读出来打印,对真想基于信息交易的人用处有限。下一步是把捕获内容建模后塞进 EA,让 MT5 在 broader 体系里做更有意义的事。 下面这段是加了校验后的核心调用与函数,逐行拆解看缓冲比对逻辑。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
        class="kw">while (!IsStopped())
        {
            Print(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);
        }
}
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); c0 < c1; c0++) if (szTest[c0] != charResultPage[iTest + c0])
        class="kw">return "Failed";
    for (class="type">int c0 = class="num">0, c1 = StringLen(szFind); c0 < c1; c0++) if (szFind[c0] != charResultPage[iPos + c0])
        class="kw">return "Error";
    for (counter = class="num">0; charResultPage[counter + iInfo] == 0x20; counter++);
    for (;charResultPage[counter + iInfo] != cLimit; counter++) szInfo += CharToString(charResultPage[counter + iInfo]);
    
    class="kw">return szInfo;
}

◍ 把网页数据喂给 EA 的下一步

这一节原本只是作者收尾,但留下的技术尾巴值得单独拎出来:抓取网页数据的关键不在请求,而在定位。上面代码里的 172783 和 173474 不是随机数字,而是下载后文件里的字节偏移地址,系统直接跳过去读,避免逐字节扫描拖慢实时行情响应。 GetDataURL 函数先用 szFind 在 iPos 处做匹配校验,不符就返回 "Error in Position";再从 iInfo 起跳过 0x20(空格)找到有效数据起点,一直读到 cLimit(本例为 0x0D 回车)截止。这套写法依赖页面结构稳定,若站方改版导致偏移变化,你得用十六进制编辑器重新找地址。 下一篇作者打算讲如何把这些抓到的数据接进 EA,会用到 MT5 里很少曝光的资源。想验证本节的,可下载文末 Scripts_de_Captura.zip(3.66 KB),把 GetDataURL 参数里的 URL 和地址换成你想抓的页面跑一遍。外汇与贵金属行情受抓取延迟影响,实盘使用前务必在模拟环境核对地址偏移,此类实时解析方案存在高风险。

MQL5 / C++
GetDataURL("https:class=class="str">"cmt">//tradingeconomics.com/stocks", class="num">100, "INDU:IND", class="num">172783, class="num">173474, 0x0D);
...
GetDataURL("https:class=class="str">"cmt">//tradingeconomics.com/stocks", class="num">100, "INDU:IND", class="num">172783, class="num">173474, 0x0D);
...
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 szFind, class="type">int iPos, class="type">int iInfo, class="type">char cLimit)
{
...
...
      for (class="type">int c0 = class="num">0, c1 = StringLen(szFind); c0 < c1; c0++) if (szFind[c0] != charResultPage[iPos + c0]) class="kw">return "Error in Position";
      for (counter = class="num">0; charResultPage[counter + iInfo] == 0x20; counter++);
      for (;charResultPage[counter + iInfo] != cLimit; counter++) szInfo += CharToString(charResultPage[counter + iInfo]);
      
      class="kw">return szInfo;
}

常见问题

先定位包含报价的稳定父节点,再用相对偏移做兜底校验;若连续两次解析失败就跳过该源,避免 EA 误用旧值。
在解析前校验回包长度与关键分隔符数量,不匹配就重试或标记数据无效,不要强行按原偏移读取。
可以,小布能定时拉取并校验关键字段,发现回包异常或偏移失效时直接提醒你,不用自己写监控脚本。
必须做类型转换与范围过滤,比如剔除非数字和明显超出品种波动带的数值,再写入 EA 的输入缓冲区。
可能是页面用了延迟行情或含运费/折算字段;优先比对页面注明的时间戳与品种代码,确认抓的是同一口径再查偏移。