从网络中获取债券收益率数据·进阶篇
(2/3)· 利率不动但债券收益率每分每秒在跳,手动抄数据做利差指标根本跟不上盘口
◍ 从网页字节流里抠出美债收益率
这段 MT5 脚本用 WinINet 拉取外部页面后,关键动作不是存盘,而是现场解析字符缓冲。它把返回字节塞进 uc_Buffer,立刻用 StringFind 定位 <meta name="price" content= 这个锚点,找不到就直接退出,避免后续越界。
锚点命中后索引硬偏移 28 位,再截取 8 个字符转成 double,得到美国 10 年期国债收益率,打印值存进 f_US。这里 28 和 8 是写死的经验值,对应页面结构里收益数字的相对位置,页面改版就会读歪。
实盘接这种外链数据,外汇与贵金属波动会受美债利率牵引,但属于高风险的间接信号,只能当概率参考。开 MT5 把这段塞进 EA 的 OnStart,改掉 URL 自己跑一遍,看 Print 出来的 f_US 跟行情源对不对得上。
bResult = InternetReadFile(iNet2, class=class="str">"cmt">// _In_ HINTERNET hFile uc_Buffer, class=class="str">"cmt">// _Out_ LPVOID lpBuffer uGet, class=class="str">"cmt">// _In_ DWORD dwNumberOfBytesToRead uGot); class=class="str">"cmt">// _Out_ LPDWORD lpdwNumberOfBytesRead Print("InternetReadFile() returned ", bResult, ". Number of bytes read: ", uGot); InternetCloseHandle(iNet2); class=class="str">"cmt">// download done if(!bResult) {InternetCloseHandle(iNet1); class="kw">return;} uc_Buffer[uGot] = class="num">0; class=class="str">"cmt">// Terminate class="type">class="kw">string in uc_Buffer by appending a null character. i = StringFind(CharArrayToString(uc_Buffer), "<meta name=\"price\" content=", class="num">0); class=class="str">"cmt">// class="num">0 == 开始搜索的位置 Print("Offset of &class="macro">#x27;<meta name=\"price\" content=&class="macro">#x27; == ", i); if(i == -class="num">1) {Print("String not found."); InternetCloseHandle(iNet1); class="kw">return;} i += class="num">28; class=class="str">"cmt">// 将索引提前到表示债券收益的文本的已知位置。 f_US = StringToDouble(StringSubstr(CharArrayToString(uc_Buffer), i, class="num">8)); Print("US class="num">10-year T-note yield, stored in variable f_US: ", f_US); InternetCloseHandle(iNet1); class=class="str">"cmt">// wininet 完成. }class=class="str">"cmt">//END class="type">void OnStart()
「当收益率不在 meta 里时扒 JSON-LD」
如果目标网页的债券收益率没有写进 meta 标签,就得直接翻 HTML 本体。先用文本编辑器搜具体报价数字,例如 10 年期美债收益率 3.061,通常能在三个非 meta 位置撞见,其中第一个是 script 类型的 JSON-LD 结构化数据块,专门喂给搜索引擎和爬虫。
算法先定位 <script type="application/ld+json"> 的偏移量,再从该位置向后找 ["price":" 与 ] 的偏移。若 ["price":" 的偏移小于 </script> 的偏移,说明报价确实落在这个数据段内,此时给 ["price":" 偏移加 9,就到了数值起始位。ScraperBot02.mq5 里跑的就是这套逻辑。
下方片段来自实际 HTML,换行后更直观:U.S. 10 Year Treasury Note 的 price 字段为 3.061,priceChange 0.007,quoteTime 标注 Sep 28, 2018 5:07 p.m.。外汇与贵金属受利率预期牵引大,这类抓取结果只反映当时快照,行情跳变属高风险常态,别当恒定基准。
<span class="tag"><<span class="keyword">script</span><span class="attribute"> type=<span class="value">"application/ld+json"</span></span>></span><span> { "@context":"http:class=class="str">"cmt">//schema.org/", "@type":"Intangible/FinancialQuote", "url":"https:class=class="str">"cmt">//www.marketwatch.com/investing/bond/tmubmusd10y?countrycode=bx", "name":"U.S. class="num">10 Year Treasury Note", "tickerSymbol":"TMUBMUSD10Y", "exchange":"Tullett Prebon", "price":"class="num">3.061", "priceChange":"class="num">0.007", "priceChangePercent":"class="num">0.22%", "quoteTime":"Sep class="num">28, class="num">2018 class="num">5:class="num">07 p.m.", "priceCurrency":"PERCENT" } </span><span class="tag"></<span class="keyword">script</span>></span>
用分块下载卡住 JSON-LD 里的价格偏移
ScraperBot02 的核心改动是给下载总量设了上限 uMax,避免把整个 HTML 页都拉下来。首次跑脚本时 uMax 要设得远大于实际体积,比如 100 万;跑完看终端报的下载字节数,如果贴近 uMax 就说明没下够,得调大。 实测某次抓取里,<script type="application/ld+json"> 标签的偏移量是 166696,于是把 uMax 设成 180224——刚好罩住 JSON-LD 片段又不必拖回整页。脚本用 16400 长度的静态数组 uc_Buffer 做 16KB 级别的块缓冲,再拷进全局动态数组 uc_DynBuf 累加。 下载循环和 01 版一样:do-while 里反复调 InternetReadFile,每次要 uGet=16384 字节,直到 uDst 达到 uMax 或连接断掉。随后从 uc_DynBuf 里先找 JSON-LD 标签偏移 i,再从 i 起找 "price":" 的偏移 j、找 </script> 偏移 k。 只要 j 小于 k,代码把 j 加 9 跳过前缀,直接截取 8 个字符用 StringToDouble 转成浮点打印。外汇与贵金属行情高度联动这类美债收益率数据,但这类抓取仅作行情参考,市场波动高风险,数值偏移可能因页面改版失效,开 MT5 跑一遍最能验证。
<span class="keyword">class="type">uchar</span> uc_Buffer[<span class="number">class="num">16400</span>], uc_DynBuf[]; <span> <span class="keyword">class="type">uint</span> uGet, uGot, uDst, uMax; uGet = class="num">16384; class=class="str">"cmt">// 每次调用InternetReadFile时要下载的字节数,必须至少比 uc_Buffer 大小小1个字节。 uGot = uDst = class="num">0; class=class="str">"cmt">// uGot是调用 InternetReadFile 时下载的字节数;uDst 是下载的字节总数。 uMax = class="num">180224; class=class="str">"cmt">// 最大下载字节数 do { bResult = InternetReadFile(iNet2, class=class="str">"cmt">// _In_ HINTERNET hFile uc_Buffer, class=class="str">"cmt">// _Out_ LPVOID lpBuffer uGet, class=class="str">"cmt">// _In_ DWORD dwNumberOfBytesToRead uGot); class=class="str">"cmt">// _Out_ LPDWORD lpdwNumberOfBytesRead uc_Buffer[uGot] = class="num">0; class=class="str">"cmt">// 使用空字符来结束 uc_Buffer 中的字符串。 <span class="functions">ArrayCopy</span>(uc_DynBuf, class=class="str">"cmt">// 目标数组 uc_Buffer, class=class="str">"cmt">// 源数组 uDst, class=class="str">"cmt">// 开始写到目标数组的索引 class="num">0, class=class="str">"cmt">// 开始从源数组复制的索引 uGot); class=class="str">"cmt">// 要复制的元素数量 uDst += uGot; class=class="str">"cmt">// 为了下一次循环调整索引 }class="kw">while(bResult && uGot > class="num">0 && uDst < uMax); <span class="functions">Print</span>("Size of uc_DynBuf == ", <span class="functions">ArraySize</span>(uc_DynBuf)); <span class="functions">Print</span>("Bytes downloaded == ", uDst); </span> <span> <span class="keyword">class="type">int</span> i, j, k; class=class="str">"cmt">// 索引 i = <span class="functions">StringFind</span>(<span class="functions">CharArrayToString</span>(uc_DynBuf), "<script type=\"application/ld+json\">", class="num">0); class=class="str">"cmt">// class="num">0 == 开始搜索的位置 <span class="functions">Print</span>("Offset of <script type=\"application/ld+json\"> == ", i); if(i == -class="num">1) {<span class="functions">Print</span>("<script type=\"application/ld+json\"> not found."); InternetCloseHandle(iNet1); class="kw">return;} j = <span class="functions">StringFind</span>(<span class="functions">CharArrayToString</span>(uc_DynBuf), "\"price\":\"", i); class=class="str">"cmt">// i == 开始搜索的位置 if(j == -class="num">1) {<span class="functions">Print</span>("\"price\":\" not found."); InternetCloseHandle(iNet1); class="kw">return;} <span class="functions">Print</span>("Offset of \"price\":\" == ", j); k = <span class="functions">StringFind</span>(<span class="functions">CharArrayToString</span>(uc_DynBuf), "</script>", i); class=class="str">"cmt">// i == 开始搜索的位置 <span class="functions">Print</span>("Offset of </script> == ", k); if(j > k) {<span class="functions">Print</span>("Offset of \"price\":\" is greater than offset of </script>"); InternetCloseHandle(iNet1); class="kw">return;} j += class="num">9; class=class="str">"cmt">// 将索引提前到表示债券收益的文本的已知位置。 f_US = <span class="functions">StringToDouble</span>(<span class="functions">StringSubstr</span>(<span class="functions">CharArrayToString</span>(uc_DynBuf), j, class="num">8)); <span class="functions">Print</span>("US class="num">10-year T-note yield, stored in variable f_US: ", f_US); InternetCloseHandle(iNet1); class=class="str">"cmt">// wininet 结束. }class=class="str">"cmt">//END class="type">void OnStart()</span><span> </span>
◍ 使用开发者工具
使用Chrome浏览器中的开发人员工具可以找到该Web服务器报价的另一个来源。从右上角的菜单按钮,打开开发者工具并输入 https://www.marketwatch.com/investment/bond/tmubmusd10y?countrycode=bx 进入地址栏。选择中间窗格中的网络选项卡,并选择“XHR”作为要监视的事件类型。选择这些事件中的任何一个将打开右侧的窗格,其中显示诸如标题和响应等详细信息。标记为“quoteByDialect…”的事件有一个有趣的响应,右键单击该窗格并选择“全选”可突出显示该响应。按Ctrl+C将突出显示的文本复制到剪贴板,然后将其粘贴到文本编辑器中。在字符串 ["CompositeTrading":{"Last":{"Price":{"Iso":"PERCENT","Value":] 后的文本块中可以找到报价。可以在“标题”选项卡下找到获取该文本块的URL。在这种情况下它有些长: https://api.wsj.net/api/dylan/quotes/v2/comp/quoteByDialect?dialect=official&needed=CompositeTrading|BluegrassChannels&MaxInstrumentMatches=1&accept=application/json&EntitlementToken=cecc4267a0194af89ca343805a3e57af&ckey=cecc4267a0&dialects=Charting&id=Bond-BX-TMUBMUSD10Y,Bond-BX-TMBMKDE-10Y . 单击本文右侧的链接将在浏览器窗口中显示文本块。它实际上是两块文本,一块接一块,因为在URL的末尾有两个由逗号分隔的断续器符号字符串。第一个“Bond-BX-TMUBMUSD10Y”用于美国10年期国债,第二个“Bond-BX-TMBMKDE-10Y”用于德国10年期国债。从URL中删除第二个断续器符号字符串会将下载文本的大小从7.1千字节减少到3.6千字节。 随附的脚本 ScraperBot03 下载了滚动条符号“TMUBMUSD10Y”的文本块,找到字符串["CompositeTrading":{"Last":{"Price":{"Iso":"PERCENT","Value":],,在字符串开头的偏移量中添加61,将其用作表示数字的文本的索引,将文本转换为浮点数并把它打印到终端中。该脚本是根据ScrapterBot01建模的,因此此处不引用代码。这个资源的一个优点是下载的规模很小,由于URL所寻址的文件只有3.6千字节,因此可以使用mql5函数WebRequest(而不是wininet.dll中的函数)来下载该文件,而无需下载远远超过需要的数据。
「用 WebRequest 直连抓取美债收益率」
这个脚本不走 WinINet,而是用 MQL5 的 WebRequest 直接拉取与旧版相同的华尔街日报债券数据。前提是先把基础域名 https://api.wsj.net 加进 MT5 的「工具→选项→专家顾问」允许 WebRequest 的服务器列表,否则调用会直接返回 -1。 代码里有个容易踩的坑:全局 char 数组 ch_Data 长度设为 16,但它并不往服务器发任何实质内容,纯粹是为了满足 WebRequest 对 const char &data[] 参数的类型要求。实际下载的数据全在 ch_Buffer 里。 请求目标锁定的是 BX-TMUBMUSD10Y,也就是美国 10 年期国债收益率。返回的是 JSON,脚本用 StringFind 定位 "CompositeTrading":{"Last":{"Price":{"Iso":"PERCENT","Value": 这段固定字符串,再把索引手动加 61 跳到数值文本起始位,取 8 个字符转成 double 存进 f_US。 验证时可以看 Print 输出:若 Server response code 不是 200 类正值,先查 GetLastError;Size of ch_Buffer 能确认实际下载字节数。外汇与贵金属受美债收益率牵引明显,但这种直连抓取依赖第三方接口稳定性,断更或改字段都会让 i==-1,实盘前务必在策略测试器里跑一遍确认。
class="type">char ch_Buffer[], ch_Data[class="num">16]; class="type">float f_US; class="type">void OnStart() { class="type">int i; class="type">class="kw">string stURL = "https:class=class="str">"cmt">//api.wsj.net/api/dylan/quotes/v2/comp/quoteByDialect?dialect=official&needed=CompositeTrading|BluegrassChannels&" "MaxInstrumentMatches=class="num">1&accept=application/json&EntitlementToken=cecc4267a0194af89ca343805a3e57af&ckey=cecc4267a0&" "dialects=Charting&id=Bond-BX-TMUBMUSD10Y"; class="type">class="kw">string stHdr = "Accept: text/*, User-Agent: Mozilla/class="num">5.0"; class="type">class="kw">string stRspHdr; class=class="str">"cmt">// 回应头 i = WebRequest("GET", class=class="str">"cmt">// class="kw">const class="type">class="kw">string method, HTTP 方法 stURL, class=class="str">"cmt">// class="kw">const class="type">class="kw">string url, URL stHdr, class=class="str">"cmt">// class="kw">const class="type">class="kw">string headers, class="num">1024, class=class="str">"cmt">// class="type">int timeout, ch_Data, class=class="str">"cmt">// class="kw">const class="type">char &data[], HTTP 消息体的数组 ch_Buffer, class=class="str">"cmt">// class="type">char &result[], 包含服务器回应数据的数组 stRspHdr); class=class="str">"cmt">// class="type">class="kw">string &result_headers Print("Server response code: ", i); if(i == -class="num">1) {Print("GetLastError == ", GetLastError()); class="kw">return;} Print("Size of ch_Buffer(bytes downloaded) == ", ArraySize(ch_Buffer)); Print("Response header:\n", stRspHdr); class="type">class="kw">string stSearch = "\"CompositeTrading\":{\"Last\":{\"Price\":{\"Iso\":\"PERCENT\",\"Value\":"; i = StringFind(CharArrayToString(ch_Buffer), stSearch, class="num">0); class=class="str">"cmt">// class="num">0 == 开始搜索的位置 Print("Offset of ", stSearch, " == ", i); if(i == -class="num">1) {Print(stSearch, " not found."); class="kw">return;} i += class="num">61; class=class="str">"cmt">// 将索引提前到表示债券收益的文本的已知位置。 f_US = StringToDouble(StringSubstr(CharArrayToString(ch_Buffer), i, class="num">8)); Print("US class="num">10-year T-note yield, stored in variable f_US: ", f_US); }class=class="str">"cmt">//END class="type">void OnStart()
拼出欧元综合利率的加权算法
做跨币种利差时,单拿德国10年期国债代表欧元利率会漏掉法国、意大利等经济体的权重影响。更扎实的做法是按欧元区内部经济体量,把几只主权债收益率做加权平均,合成一个“欧洲债券”利率。 以2017年GDP为权重基准,德国、法国、意大利的国内生产总值分别为3197、2241、1681十亿欧元,合计7119十亿欧元。由此算出的加权系数依次是德国0.449、法国0.315、意大利0.236,三项相加恰好为1。 合成公式直接写成:f_EU = 0.449*f_DE + 0.315*f_FR + 0.236*f_IT。把德法意三国10年期收益率按上述系数代入,就能得到综合欧元利率,用于和美债利差做对比。外汇与贵金属受利差驱动明显,但价差走法受多变量干扰,实际共振方向仅具概率性,交易前请在MT5手动核对各收益率源。