从网络中获取债券收益率数据·进阶篇
📡

从网络中获取债券收益率数据·进阶篇

(2/3)· 利率不动但债券收益率每分每秒在跳,手动抄数据做利差指标根本跟不上盘口

进阶 第 2/3 篇
很多交易者把央行利率当唯一基本面锚点,却忽略10年期国债收益率在盘中持续重定价。靠肉眼刷网页抄数字做利差,不仅慢还容易漏掉跳空,EA跑出来的信号早已失真。

◍ 从网页字节流里抠出美债收益率

这段 MT5 脚本用 WinINet 拉取外部页面后,关键动作不是存盘,而是现场解析字符缓冲。它把返回字节塞进 uc_Buffer,立刻用 StringFind 定位 <meta name="price" content= 这个锚点,找不到就直接退出,避免后续越界。 锚点命中后索引硬偏移 28 位,再截取 8 个字符转成 double,得到美国 10 年期国债收益率,打印值存进 f_US。这里 28 和 8 是写死的经验值,对应页面结构里收益数字的相对位置,页面改版就会读歪。 实盘接这种外链数据,外汇与贵金属波动会受美债利率牵引,但属于高风险的间接信号,只能当概率参考。开 MT5 把这段塞进 EA 的 OnStart,改掉 URL 自己跑一遍,看 Print 出来的 f_US 跟行情源对不对得上。

MQL5 / C++
  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.。外汇与贵金属受利率预期牵引大,这类抓取结果只反映当时快照,行情跳变属高风险常态,别当恒定基准。

MQL5 / C++
<span class="tag">&lt;<span class="keyword">script</span><span class="attribute"> type=<span class="value">"application/ld+json"</span></span>&gt;</span><span>
{ "@context":"http:class=class="str">"cmt">//schema.org/",
&nbsp;&nbsp;"@type":"Intangible/FinancialQuote",
&nbsp;&nbsp;"url":"https:class=class="str">"cmt">//www.marketwatch.com/investing/bond/tmubmusd10y?countrycode=bx",
&nbsp;&nbsp;"name":"U.S. class="num">10 Year Treasury Note",
&nbsp;&nbsp;"tickerSymbol":"TMUBMUSD10Y",
&nbsp;&nbsp;"exchange":"Tullett Prebon",
&nbsp;&nbsp;"price":"class="num">3.061",
&nbsp;&nbsp;"priceChange":"class="num">0.007",
&nbsp;&nbsp;"priceChangePercent":"class="num">0.22%",
&nbsp;&nbsp;"quoteTime":"Sep class="num">28, class="num">2018 class="num">5:class="num">07 p.m.",
&nbsp;&nbsp;"priceCurrency":"PERCENT"
}
</span><span class="tag">&lt;/<span class="keyword">script</span>&gt;</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 跑一遍最能验证。

MQL5 / C++
<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 &amp;&amp; uGot &gt; class="num">0 &amp;&amp; uDst &lt; 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), "&lt;script type=\"application/ld+json\"&gt;", class="num">0); class=class="str">"cmt">// class="num">0 == 开始搜索的位置 
  <span class="functions">Print</span>("Offset of &lt;script type=\"application/ld+json\"&gt; == ", i); 
  if(i == -class="num">1) {<span class="functions">Print</span>("&lt;script type=\"application/ld+json\"&gt; 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), "&lt;/script&gt;", i); class=class="str">"cmt">// i == 开始搜索的位置
  <span class="functions">Print</span>("Offset of &lt;/script&gt; == ", k); 
  if(j &gt; k) {<span class="functions">Print</span>("Offset of \"price\":\" is greater than offset of &lt;/script&gt;");  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,实盘前务必在策略测试器里跑一遍确认。

MQL5 / C++
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手动核对各收益率源。

把收益率抓取交给小布盯盘
这些跨站抓取与利率差计算,小布盯盘的 AIGC 已内置到对应货币对页,打开就能直接看派生指标,你只管判断方向。

常见问题

数字可能由脚本动态生成,需通过浏览器开发工具抓脚本发出的真实数据URL,再让程序直接请求该地址解析。
10年期更反映市场对远期央行路径的预期,与EUR/USD等pair的中长周期波动相关性倾向更强,但短线噪声也更大。
频率过高可能触发站点限流,建议拉大请求间隔并缓存结果,具体风险概率随目标站点的防护策略变化。
可以,小布盯盘内置了多期限收益率的抓取与利差计算,切换品种即见对应衍生数据,省去自己写解析。
Investing、Treasury.gov等也提供结构化页面,但各站HTML差异大,抓取规则需按站重写,外汇贵金属波动高风险需注意数据延迟。