为 MetaTrader 5 创建自定义新闻递送·综合运用
📡

为 MetaTrader 5 创建自定义新闻递送·综合运用

(3/3)· 把 NewsAPI 接进 MT5 的 18 个步骤,解决经纪商新闻源不可控的老问题

含代码示例实战向 第 3/3 篇
经纪商内置新闻常常要么缺失要么滞后,盯盘时想看特定媒体头条却得切出去刷网页。用 NewsAPI 把多源新闻拉进 MT5 面板,筛选和刷新都能自己定。本篇顺着前两篇的基础,把整套程序拼到能跑为止。

新闻接口返回的结构与报错

这段原始响应里混了三组内容:前一段是 6 条 JSON 格式的新闻条目,后两段是接口报错文本。新闻条目里每条都带 author、title、publishedAt 等字段,例如 2017-11-11T14:00:56Z 发布的导弹产能报道,以及同日的 GOP 减税评论,说明数据源原本按时间倒序吐结构。 紧接着的报错显示 apiKeyInvalid,意思是密钥校验没过;再后面一条 sourceDoesntExist,指出传入的 news source 不在合法列表。对写行情插件的人来说,这种多状态混回的情况必须在解析前先判 status 字段,否则会把报错当新闻写进数组。 在 MT5 里若用 WebRequest 拉外源新闻做情绪标记,应当先筛掉 code 非 ok 的包,只保留有 publishedAt 与 url 的实体,避免把密钥错误污染到信号逻辑。外汇与贵金属受突发新闻驱动明显,但外源接口稳定性差,实盘使用前建议在策略测试器里跑一周回放验证丢包率。

MQL5 / C++
[CODE]
[
{
"author":"Morgan Brennan","title":"With an eye toward the North Korean threat, a &class="macro">#x27;missile renaissance&class="macro">#x27; blooms in the US","description":"Raytheon is cranking out about class="num">20 Standard Missile variants per month, as part of the effort to help repel a possible attack from North Korea.","url":"https:class=class="str">"cmt">//www.cnbc.com/class="num">2017/class="num">11/class="num">11/north-korea-threat-leads-to-a-us-missile-renaissance.html","urlToImage":"https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/class="num">2017/class="num">08/class="num">03/class="num">104631031-RTS1A3SU.1910x1000.jpg","publishedAt":"class="num">2017-class="num">11-11T14:class="num">00:56Z"},
{"author":"Larry Kudlow","title":"Larry Kudlow: A pro-growth GOP tax cut is on the way — this year","description":"One way or another, Congress will come up with a significant pro-growth bill, writes Larry Kudlow.","url":"https:class=class="str">"cmt">//www.cnbc.com/class="num">2017/class="num">11/class="num">11/larry-kudlow-pro-growth-gop-tax-cut-is-on-the-way--this-year.html","urlToImage":"https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/class="num">2017/class="num">11/class="num">02/class="num">104816986-GettyImages-class="num">869498942.1910x1000.jpg","publishedAt":"class="num">2017-class="num">11-11T14:class="num">22:58Z"},
{"author":"Reuters","title":"Trans-Pacific trade deal advances without United States","description":"Last-minute resistance from Canada had raised new doubts about its survival.","url":"https:class=class="str">"cmt">//www.cnbc.com/class="num">2017/class="num">11/class="num">11/trans-pacific-trade-deal-advances-without-united-states.html","urlToImage":"https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/class="num">2016/class="num">11/class="num">22/class="num">104123278-GettyImages-class="num">624177112.1910x1000.jpg","publishedAt":"class="num">2017-class="num">11-11T10:class="num">23:03Z"},
{"author":"Jacob Pramuk","title":"McConnell says he &class="macro">#x27;misspoke&class="macro">#x27; about middle-class tax hikes","description":"Mitch McConnell told The New York Times that \"you can&class="macro">#x27;t guarantee that no one sees a tax increase.\"","url":"https:class=class="str">"cmt">//www.cnbc.com/class="num">2017/class="num">11/class="num">10/mitch-mcconnell-says-he-misspoke-about-republican-tax-plan.html","urlToImage":"https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/class="num">2017/class="num">09/class="num">22/class="num">104726784-RTX3FY40-mcconnell.1910x1000.jpg","publishedAt":"class="num">2017-class="num">11-10T23:class="num">04:47Z"},
{"author":"Erin Barry","title":"Start-up Dia&Co is catering the class="num">70 percent of US women the fashion industry ignores","description":"There are more than class="num">100 million plus-size women in the U.S., but finding fashionable clothes in their size can be a challenge.","url":"https:class=class="str">"cmt">//www.cnbc.com/class="num">2017/class="num">11/class="num">10/diaco-caters-to-the-class="num">70-percent-of-us-women-fashion-ignores.html","urlToImage":"https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/class="num">2017/class="num">02/class="num">22/class="num">104298276-Lydia-Gilbert--Nadia-Boujarwah-2_credit-DiaCo_r.1910x1000.jpg","publishedAt":"class="num">2017-class="num">11-11T14:class="num">01:01Z"},
{"author":"Elizabeth Gurdus","title":"Cramer shares a little-known investing concept critical to buying stocks","description":"Jim Cramer explained why the idea of suitability is crucial when it comes to individual investing.","url":"https:class=class="str">"cmt">//www.cnbc.com/class="num">2017/class="num">11/class="num">10/cramer-shares-an-investing-concept-critical-to-buying-stocks.html","urlToImage":"https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/class="num">2017/class="num">11/class="num">10/class="num">104835669-GettyImages-class="num">825493934.1910x1000.jpg","publishedAt":"class="num">2017-class="num">11-10T23:class="num">10:58Z"}
]
{
"status":"error",
"code":"apiKeyInvalid",
"message":"Your API key is invalid or incorrect. Check your key, or go to https:class=class="str">"cmt">//newsapi.org to create a free API
key."
}
{
<span class="class="type">class="kw">string">"status"</span>:<span class="class="type">class="kw">string">"error"</span>,
<span class="class="type">class="kw">string">"code"</span>:<span class="class="type">class="kw">string">"sourceDoesntExist"</span>,
<span class="class="type">class="kw">string">"message"</span>:<span class="class="type">class="kw">string">"The news source you&class="macro">#x27;ve entered doesn&class="macro">#x27;t exist. Check your spelling, or see /v1/sources for a list of valid sources."</span>
}
[/CODE]

「两个 JSON 库在 MT5 下的实测差异」

在 MT5 里处理新闻接口返回的 JSON 元数据,代码库里能直接拿到的有两个选择:json.mqh 和 JAson.mqh。先试 json.mqh,编译测试脚本 newstest_json 时蹦出若干错误和警告,问题出在函数库文件自身——作者把维护挪到了 github,但代码库里的文件没同步更新。 修这些编译错误不麻烦,把哈希头文件 hash.mqh 直接 include 进 json.mqh 即可消掉错误,警告基本来自隐式类型转换。改完再跑 newstest_json,脚本读入之前 API 调用存下的 JSON 文件,把里面所有可用新闻渠道打印到终端,功能正常。 换 JAson.mqh 跑 newstest_JAson,首次编译零错误零警告,开箱即用。两者都覆盖全部 JSON 数据类型,但结构思路不同:json.mqh 给每种类型都建一个类(JSONString、JSONNumber 等),JAson.mqh 只用单个类,类型靠可公开访问的属性区分。 后续要做 MT5 新闻展示程序,选 JAson.mqh 更省事。它会被写成一个 EA,列出新闻源,点选某源后旁边文本框拉取该渠道最新摘要。外汇与贵金属行情受新闻扰动大、杠杆风险高,这类读取仅作信息辅助,不预示走向。

MQL5 / C++
class=class="str">"cmt">// $Id: json.mqh class="num">102 class="num">2014-class="num">02-class="num">24 class="num">03:class="num">39:28Z ydrol $
class="macro">#include "hash.mqh"
class="macro">#ifndef YDROL_JSON_MQH
class="macro">#define YDROL_JSON_MQH
class=class="str">"cmt">// (C)class="num">2014 Andrew Lord forex@NICKNAME@lordy.org.uk
class=class="str">"cmt">// 解析一个 JSON 字符串 - 适用于 mql4++, 来自我的 gawk 实现
class=class="str">"cmt">// ( https://code.google.com/p/oversight/source/browse/trunk/bin/catalog/json.awk )
class=class="str">"cmt">/*
  TODO 常量 true|class="kw">false|null 可以表示为固化对象。
      为此, 删除 _hash 和 _array 必须跳过这些对象。
  TODO 测试 null
  TODO 解析 Unicode 转义
*/
class=class="str">"cmt">/*
  参见 json_demo 示例。
这会需要 hash.mqh( http:class=class="str">"cmt">//codebase.mql4.com/class="num">9238 , http://lordy.co.nf/hash )
*/
enum ENUM_JSON_TYPE { JSON_NULL,JSON_OBJECT,JSON_ARRAY,JSON_NUMBER,JSON_STRING,JSON_BOOL };
class JSONString;
class=class="str">"cmt">// 所有 JSON 类型的通用类 (Number, String, Bool, Array, Object )
class JSONValue : class="kw">public HashValue
  {
class="kw">private:
  ENUM_JSON_TYPE      _type;
class="kw">public:
                     JSONValue() {}
                    ~JSONValue() {}
  ENUM_JSON_TYPE getType() { class="kw">return _type; }
  class="type">void setType(ENUM_JSON_TYPE t) { _type=t; }
  class=class="str">"cmt">// 类型方法
  class="type">bool isString() { class="kw">return _type==JSON_STRING; }
  class="type">bool isNull() { class="kw">return _type==JSON_NULL; }
  class="type">bool isObject() { class="kw">return _type==JSON_OBJECT; }
  class="type">bool isArray() { class="kw">return _type==JSON_ARRAY; }
  class="type">bool isNumber() { class="kw">return _type==JSON_NUMBER; }
  class="type">bool isBool() { class="kw">return _type==JSON_BOOL; }
  class=class="str">"cmt">// 在子类中覆盖
  class="kw">virtual class="type">class="kw">string toString()
   {
     class="kw">return "";
   }
  class=class="str">"cmt">// 一些便于获得子类型投射的取值函数。
  class="type">class="kw">string getString()
   {
     class="kw">return ((JSONString *)GetPointer(this)).getString();
   }
  class="type">class="kw">double getDouble()
   {
     class="kw">return ((JSONNumber *)GetPointer(this)).getDouble();
   }
  class="type">long getLong()
   {
     class="kw">return ((JSONNumber *)GetPointer(this)).getLong();
   }
  class="type">int getInt()
   {
     class="kw">return ((JSONNumber *)GetPointer(this)).getInt();
   }
  class="type">bool getBool()
   {
     class="kw">return ((JSONBool *)GetPointer(this)).getBool();
   }
  class=class="str">"cmt">// 由数组和对象调用的静态取值函数 当返回子类结果时。
  class=class="str">"cmt">// 它们允许应用程序检查数值且检索时不必停止程序 
  class=class="str">"cmt">// (有时候程序暂停是需要的 - 而非 EA 停止, 然后继续处理错误数据)
  class="kw">static class="type">bool getString(JSONValue *val,class="type">class="kw">string &out)
   {
     if(val!=NULL && val.isString())
       {
        out = val.getString();
        class="kw">return true;
       }
     class="kw">return class="kw">false;
   }
  class="kw">static class="type">bool getBool(JSONValue *val,class="type">bool &out)
   {

◍ JSON 取值函数的类型守卫写法

在 MT5 里解析外部 JSON 行情或新闻流时,最怕拿到空指针或类型错配导致脚本崩在 OnStart。上面这组静态方法把布尔、双精度、长整、整型分开提取,每个都先做 val!=NULL 和对应 is 判断,再写引用出参并返回 true,否则直接返回 false。 以 getDouble 为例:val.isNumber() 为真才执行 out = val.getDouble(),否则不改动 out。这种写法让你在解析财经日历 JSON 时,对某个可能缺失的浮点字段(例如某平台 Tick 体积字段)可以安全跳过,而不用每处都包 try-catch。 实际验证路径:把 json.mqh 放进 MQL5/Include,新建脚本抄这段 Test 骨架,给 pStream 喂一段含 bool 与 number 的测试串,断点看 getBool / getLong 返回 false 的占比;外汇与贵金属接口字段经常变更,这类守卫能显著降低 EA 在实盘中的非预期终止概率。

MQL5 / C++
if(val!=NULL && val.isBool())
  {
   out = val.getBool();
   class="kw">return true;
  }
 class="kw">return class="kw">false;
  }
 class="kw">static class="type">bool getDouble(JSONValue *val,class="type">class="kw">double &out)
  {
   if(val!=NULL && val.isNumber())
     {
      out = val.getDouble();
      class="kw">return true;
     }
   class="kw">return class="kw">false;
  }
 class="kw">static class="type">bool getLong(JSONValue *val,class="type">long &out)
  {
   if(val!=NULL && val.isNumber())
     {
      out = val.getLong();
      class="kw">return true;
     }
   class="kw">return class="kw">false;
  }
 class="kw">static class="type">bool getInt(JSONValue *val,class="type">int &out)
  {
   if(val!=NULL && val.isNumber())
     {
      out = val.getInt();
      class="kw">return true;
     }
   class="kw">return class="kw">false;
  }
};

从 sources.txt 捞数据进 JSON 解析器

这段逻辑演示了如何在 MT5 脚本里把外部文本文件读入内存,再交给 JAson 库做结构化解析。文件名为 sources.txt,用 FILE_TXTFILE_READFILE_UNICODE 三标志打开,若返回 INVALID_HANDLE 直接打印错误并退出,避免后续空指针崩脚本。

循环读取时先用 FileReadInteger(hFile, INT_VALUE) 拿字符串长度,再 FileReadString(hFile, iStringSize) 取内容拼到 pStream。这里有个细节:Unicode 文本头部带长度前缀,所以必须按 INT_VALUE 读长度,否则乱码概率很高。 读完关句柄后 new 一个 JSONParser,parse(pStream) 拿到 JSONValue。若返回 NULL 就打印 getErrorCode() 和 getErrorMessage();否则判断 isObject(),从键 "sources" 取数组,挨个 Print 出每个元素的 "id" 字段。 开 MT5 验证时,把下面代码塞进脚本的 Test() 函数,终端 Common 目录放一份含 {"sources":[{"id":"x1"}]} 的 sources.txt,跑起来能在日志看到 x1。外汇与贵金属行情受杠杆影响大,这类文件读取仅用于本地数据分析,实盘信号请自担高风险。

MQL5 / C++
  class="type">class="kw">string sources_filename="sources.txt";
  class="type">int hFile,iStringSize;
class=class="str">"cmt">// 读取文件内容 
  hFile=::FileOpen;
  if(hFile==INVALID_HANDLE)
    {
      ::Print("打开文件错误 "+sources_filename);
      class="kw">return(class="kw">false);
    }
  while(!::FileIsEnding(hFile))
    {
      iStringSize = ::FileReadInteger(hFile, INT_VALUE);
      pStream    += ::FileReadString(hFile, iStringSize);
    }
  ::FileClose(hFile);
  Print("打开并读取文件成功");
  JSONParser *parser=new JSONParser();
  JSONValue *jv=parser.parse(pStream);
  if(jv==NULL)
    {
      Print("错误:"+(class="type">class="kw">string)parser.getErrorCode()+parser.getErrorMessage());
        } else {
      if(jv.isObject())
        {
         JSONObject *jo = jv;
         JSONArray  *jd =  jo.getArray("sources");
         for(class="type">int i=class="num">0;i<jd.size();i++)
          {
            Print(jd.getObject(i).getString("id"));
          }
        }
      class="kw">delete jv;
    }
  class="kw">delete parser;
  class="kw">return(true);
  }

「从 sources.txt 反序列化出信号源清单」

做多源行情聚合时,先把外部配置落盘成 Unicode 文本是最稳的做法。下面这段逻辑演示了如何在 MT5 里读取 sources.txt、拼成完整字符串再交给 CJAVal 做 JSON 解析,省去硬编码数据源的麻烦。

文件用 FILE_TXTFILE_READFILE_UNICODE 打开,若句柄等于 INVALID_HANDLE 直接打印错误并退出,避免后续空指针。循环里靠 FileReadInteger 拿当前字符串字节长度,再用 FileReadString 按长度读入并累加进 pStream,读完 FileClose 收尾。

pStream 交给 srce.Deserialize 后,取 srce["sources"] 构造 CJAVal 数组,遍历打印每个元素的 id 字段。外汇与贵金属行情源常因报价商差异而字段不齐,这种读取方式在切换信号源时只需改 txt 不必动 EA 主体,但多源并发仍有断流风险,实盘前请在策略测试器跑至少 200 根 Tick 验证。

MQL5 / C++
  class="type">class="kw">string pStream;
  class="type">class="kw">string sources_filename="sources.txt";
  class="type">int hFile,iStringSize;
class=class="str">"cmt">// 读取文件内容 
  hFile=::FileOpen;
  if(hFile==INVALID_HANDLE)
    {
      ::Print("打开文件错误 "+sources_filename);
      class="kw">return(class="kw">false);
    }
  while(!::FileIsEnding(hFile))
    {
      iStringSize = ::FileReadInteger(hFile, INT_VALUE);
      pStream    += ::FileReadString(hFile, iStringSize);
    }
  ::FileClose(hFile);
  Print("打开并读取文件成功");
  CJAVal  srce;
  if(!srce.Deserialize(pStream))
    {
      ::Print("Json 析构错误");
      class="kw">return(class="kw">false);
    }
  CJAVal *json_array=new CJAVal(srce["sources"]);
  for(class="type">int i=class="num">0;i<ArraySize(json_array.m_e);i++)
    {
      Print(json_array[i]["id"].ToStr());
    }
  class="kw">delete json_array;
  class="kw">return(true);
}

◍ 用 CNewsFeed 搭一个极简新闻面板

做 MT5 新闻面板时,GUI 框架别自己造。Anatoli Kazharski 的 EasyAndFastGUI 持续在迭代,版本分支多到混乱;实测「图形界面 XI: 重构函数库代码」那个分支最干净,该有的控件齐全,没多余依赖,适合塞进 EA 或脚本里跑。 我们的需求很裸:不需要菜单栏、不需要选项卡,只要主窗体 + 列表视图 + 文本框 + 状态栏。头文件 NewsFeedprogram.mqh 同时 include 了 GUI 库和 JAson 库,URL 拼接指令直接用宏写死,例如 BASE_URL 指向 newsapi.org/v1/,ATCLE 拼成 articles? LATEST 和 API_KEY。 主类 CNewsFeed 从 CWndEvents 派生。私有成员里,窗体、列表、文本框、状态栏是界面骨架;一个 CTimeCounter 专门刷状态栏时间。CJAVal 类型的 srce 和 js 分别接住新闻源返回的 json 和单渠道正文,其余指针探进嵌套 json。下面这段代码就是类声明的骨架,复制进 MT5 头文件即可编译验证。 外汇与贵金属消息面波动大、滑点风险高,新闻源延迟可能引发错单,面板数据仅作辅助参考。

MQL5 / C++
class="macro">#class="kw">property copyright "版权所有 class="num">2017, ufranco"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <EasyAndFastGUI\WndEvents.mqh>
class="macro">#include <EasyAndFastGUI\TimeCounter.mqh>
class="macro">#include <JAson.mqh>
class="macro">#define BASE_URL "https:class=class="str">"cmt">//newsapi.org/v1/"
class="macro">#define SRCE "sources?"
class="macro">#define ATCLE "articles?
class="macro">#define LATEST "&sortBy=latest"
class="macro">#define API_KEY "&apiKey=484c84eb9765418fb58ea936908a47ac"
class CNewsFeed : class="kw">public CWndEvents
{
class="kw">private:
   CWndForm        m_form;          class=class="str">"cmt">// 主窗口窗体
   CWndListView    m_list;          class=class="str">"cmt">// 列表视图控件
   CWndText        m_text;          class=class="str">"cmt">// 文本框控件
   CWndStatusBar   m_status;        class=class="str">"cmt">// 状态栏控件
   CTimeCounter    m_timer;         class=class="str">"cmt">// 状态栏计时器
   CJAVal          srce;            class=class="str">"cmt">// 新闻源 json 对象
   CJAVal          js;              class=class="str">"cmt">// 单渠道新闻 json 对象
   CJAVal         *m_ptr;           class=class="str">"cmt">// 嵌套 json 指针
};

新闻面板类的私有成员布局

在 MT5 里用 EasyAndFastGUI 搭一个实时新闻面板,核心是先定义好 CNewsFeed 这个继承自 CWndEvents 的类。它私有段里挂的全是界面与数据对象:CTimeCounter 负责状态栏的定时刷新,CWindow 是主窗体,CStatusBar、CListView、CTextBox 分别管状态栏、新闻列表和编辑框。 数据侧用 CJAVal(JAson 库)做 JSON 解析,srce 与 js 是两份主对象,另外四个 CJAVal 指针 articles、articlesArrayElement、sources、sourcesArrayElement 专门指向嵌套数组元素,避免每次遍历都重新查键。 宏里把接口基址写死成 https://newsapi.org/v1/,并拼了 sources?、articles? 以及带 apiKey 的查询串。实盘接外部新闻源属于第三方依赖,外汇和贵金属本身波动剧烈、叠加消息面跳空风险高,密钥硬编码在代码里也只是示例用法,自己部署时建议抽成输入参数。

MQL5 / C++
class CNewsFeed : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 时间计数器
   CTimeCounter       m_counter; class=class="str">"cmt">// 用于更新状态栏中的项目
   class=class="str">"cmt">//--- 主窗口
   CWindow            m_window;
   class=class="str">"cmt">//--- 状态栏
   CStatusBar         m_status_bar;
   class=class="str">"cmt">//--- 列表视图
   CListView          m_listview;
   class=class="str">"cmt">//--- 编辑
   CTextBox           m_text_box;
   class=class="str">"cmt">//--- 主 Json 对象 
   CJAVal             srce;
   CJAVal             js;
   class=class="str">"cmt">//--- Json 指针指向引用的嵌套元素
   CJAVal            *articles;
   CJAVal            *articlesArrayElement;
   CJAVal            *sources;
   CJAVal            *sourcesArrayElement;
class="kw">public:

「新闻聚合面板的类接口骨架」

想在 MT5 里做一个自动抓取财经消息的面板,先得把类结构立住。下面这段声明定义了 CNewsFeed 的公开与私有方法,公开部分管生命周期和事件,私有部分管界面拼装与数据下载。 构造与析构成对出现,OnInitEvent 返回 bool 方便初始化失败时直接退出;OnDeinitEvent 带 reason 参数,能区分是手动移除还是重编译。OnTimerEvent 留给定时轮询,OnEvent 是虚函数,图表点击、自定义事件都能往下传。 私有方法里 CreateWindow、CreateStatusBar、CreateListView、CreateTextBox 四个函数各自吃 x_gap、y_gap 坐标偏移,说明界面是绝对定位手摆的,不是布局器。PrepareSources 先把源站 Json 填好,PrepareArticles 按列表选中项再拉具体文章,下载统一走 DownLoad 返回本地文件名。 直接把这段声明贴进 MQ5 文件,补全实现后挂 EA 上,就能在策略测试器外跑出带界面的新闻流;外汇与贵金属消息面波动大,实盘使用前建议在模拟账户验证接口稳定性。

MQL5 / C++
CNewsFeed(class="type">void);
~CNewsFeed(class="type">void);
class=class="str">"cmt">//--- 初始化/逆初始化
class="type">bool OnInitEvent(class="type">void);
class="type">void OnDeinitEvent(class="kw">const class="type">int reason);
class=class="str">"cmt">//--- 定时器
class="type">void OnTimerEvent(class="type">void);
class=class="str">"cmt">//--- 图表事件处理器
class="kw">virtual class="type">void OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam);
class=class="str">"cmt">//--- 创建程序的图形界面
class="type">bool CreateGUI(class="type">void);
class="kw">private:
class=class="str">"cmt">//--- 主窗口
class="type">bool CreateWindow(class="kw">const class="type">class="kw">string text);
class=class="str">"cmt">//--- 状态栏
class="type">bool CreateStatusBar(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap);
class=class="str">"cmt">//--- 列表视图
class="type">bool CreateListView(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap);
class=class="str">"cmt">//--- 编辑
class="type">bool CreateTextBox(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap);
class=class="str">"cmt">//--- 填充 sources Json 对象
class="type">bool PrepareSources(class="type">void);
class=class="str">"cmt">//--- 根据 sources Json 对象中的选定元素填充 articles Json 对象
class="type">bool PrepareArticles(class="type">int listview_index);
class=class="str">"cmt">//--- 下载数据
class="type">class="kw">string DownLoad(class="type">class="kw">string url,class="type">class="kw">string file_name);
};

◍ 修掉 CTextBox 刷新失灵的坑

在 MT5 的 GUI 函数库里,CTextBox 控件的 update() 方法有个隐性 bug:文本框内容被动态改写时,界面不会按预期重绘,最小化再最大化后更容易暴露问题。根因大多出在自动调整逻辑和初始 size 参数上——一旦新内容行宽或行数超过初始化时设定的最大值,水平或垂直滚动条就会部分失灵,临界点之后滚动内容突然消失。 实测可行的改法是:在 update() 里补进同类的私有方法调用,并主动开启自动换行(WordWrap),用空行把文本框预先撑大,同时关掉列表视图与文本框的垂直自动调整。这样就不再依赖水平滚动条,刷新内容也不会把滚动搞崩。 下面这段是改过的 Update 方法,注意 redraw 为真时先 WordWrap 再 CalculateTextBoxSize 和 ChangeScrollsSize,原版被注释掉的 ChangeTextBoxSize 不再走。直接拿去替换库里的 TextBox.mqh 就能在 MT5 里验证刷新表现。 外汇与贵金属界面开发同样受 MT5 框架限制,改动 GUI 库属高风险调试,建议在策略测试器沙盘先跑通再上实盘终端。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 更新控件                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTextBox::Update(class="kw">const class="type">bool redraw=class="kw">false)
  {
class=class="str">"cmt">//--- 如果指定的话, 重绘表格
   if(redraw)
     {
class=class="str">"cmt">//--- 绘图
class=class="str">"cmt">//ChangeTextBoxSize();
      WordWrap();
      Draw();
      CalculateTextBoxSize();
      ChangeScrollsSize();
class=class="str">"cmt">//--- 应用
      m_canvas.Update();
      m_textbox.Update();
      class="kw">return;
     }
class=class="str">"cmt">//--- 应用
   m_canvas.Update();
   m_textbox.Update();
  }

EA 里 Json 新闻源的拉取与离线兜底

做 MT5 新闻类 EA 时,网络请求和本地缓存必须分开设计。Download() 用 WebRequest 发 GET,超时写死 5000 毫秒,断连直接返回 NULL;若传了第二个文件名参数,就把响应流写进二进制文件,方便后续离线反序列化。 PrepareSources() 只在 EA 初始化跑一次,拼 BASE_URL+SRCE 去拉源列表,落盘为 sources.txt,再交给 Json 解析器的 Deserialize 填进 sources 指针。这里有个硬约束:若当时没连上、且 sources.txt 还没生成,EA 会初始化失败,不会静默空跑。 PrepareArticles() 放在主图表事件里,用列表选中项的索引拼出对应渠道的 URL 再请求。注意两种 Prepare 方法都依赖「先有过一次联网且文件已落盘」才能离线工作,否则无网即废。外汇与贵金属行情受新闻扰动大,这类 EA 实盘前务必在模拟盘验证断网表现,滑点与延迟风险偏高。 下面这段是核心拉取与初始化代码,逐行看逻辑落点: //+------------------------------------------------------------------+ //| Web 请求和缓存数据的方法 //+------------------------------------------------------------------+ string CNewsFeed::DownLoad(string url,string file_name="") { // 如果终端未连接 EA, 则假定没有连接 if(!(bool)::TerminalInfoInteger(TERMINAL_CONNECTED))return(NULL); string cookie=NULL,headers,pStream; char post[],result[]; int res,hFile; ::ResetLastError(); int timeout=5000; // web 请求 res=::WebRequest("GET",url,cookie,NULL,timeout,post,0,result,headers); if(res==-1) { ::Print("WebRequest 失败"); return(NULL); } // 下载数据流 pStream=::CharArrayToString(result,0,-1,CP_UTF8); if(file_name!="") { hFile=::FileOpen(file_name,FILE_BIN|FILE_WRITE); if(hFile==INVALID_HANDLE) { return(pStream); ::Print("无效文件句柄 - "+file_name+" - 无法保存数据至文件"); } // 将下载的数据写入文件 ::FileWriteString(hFile,pStream); ::FileClose(hFile); } //Print("下载成功"); return(pStream); } //+------------------------------------------------------------------+ //| 下载数据填充 sources json 对象 //+------------------------------------------------------------------+ bool CNewsFeed::PrepareSources(void) { string sStream; int iStringSize,hFile; string sources_filename="sources.txt"; // 下载数据 sStream=DownLoad(BASE_URL+SRCE,sources_filename); if(sStream==NULL) { if(!::FileIsExist) { ::Print("错误 : 所需文件不存在"); return(false); } // 读取文件内容 hFile=::FileOpen; if(hFile==INVALID_HANDLE) {

MQL5 / C++
class="type">class="kw">string CNewsFeed::DownLoad(class="type">class="kw">string url,class="type">class="kw">string file_name="")
  {
   if(!(class="type">bool)::TerminalInfoInteger(TERMINAL_CONNECTED))class="kw">return(NULL);
   class="type">class="kw">string cookie=NULL,headers,pStream;
   class="type">char post[],result[];
   class="type">int res,hFile;
   ::ResetLastError();
   class="type">int timeout=class="num">5000;
   res=::WebRequest("GET",url,cookie,NULL,timeout,post,class="num">0,result,headers);
   if(res==-class="num">1)
     {
      ::Print("WebRequest 失败");
      class="kw">return(NULL);
     }
   pStream=::CharArrayToString(result,class="num">0,-class="num">1,CP_UTF8);
   if(file_name!="")
     {
      hFile=::FileOpen(file_name,FILE_BIN|FILE_WRITE);
      if(hFile==INVALID_HANDLE)
        {
         class="kw">return(pStream);
         ::Print("无效文件句柄 - "+file_name+" - 无法保存数据至文件");
        }
      ::FileWriteString(hFile,pStream);
      ::FileClose(hFile);
     }
   class="kw">return(pStream);
  }
class="type">bool CNewsFeed::PrepareSources(class="type">void)
  {
   class="type">class="kw">string sStream;
   class="type">int    iStringSize,hFile;
   class="type">class="kw">string sources_filename="sources.txt";
   sStream=DownLoad(BASE_URL+SRCE,sources_filename);
   if(sStream==NULL)
     {
      if(!::FileIsExist)
        {
         ::Print("错误 : 所需文件不存在");
         class="kw">return(class="kw">false);
        }
      hFile=::FileOpen;
      if(hFile==INVALID_HANDLE)
        {

「按资源 ID 拉取新闻并落盘读取」

在 CNewsFeed::PrepareArticles 里,先对 sources 指针和 listview_index 做防御:sources 为 NULL 或索引超过 ArraySize 会直接返回 false,避免越界崩 EA。这一步决定了后面能不能安全拿到具体新闻源的 id。

拿到 id 后调用 DownLoad(BASE_URL+ATCLE+id+API_KEY, id+".txt") 把该资源的新闻 JSON 下载到本地 txt。若返回 NULL 且 FileIsExist 也找不到文件,就报“所需文件不存在”并退出;若文件在,则走 FileOpen 以 FILE_TXTFILE_READFILE_UNICODE 重新打开。

读取循环用 while(!FileIsEnding(hFile)) 配合 FileReadInteger(hFile, INT_VALUE) 取每段字符串长度,再 FileReadString 拼进 sStream。外汇与贵金属新闻源受网络与 API 权限影响,读取失败概率偏高,实盘前建议在策略测试器里先断网跑一遍文件回退逻辑。 下面这段是文件回退读取的核心片段,注意 INT_VALUE 控制的是每次读几个字节的长度头:

MQL5 / C++
while(!::FileIsEnding(hFile))
  {
   iStringSize = ::FileReadInteger(hFile, INT_VALUE);

◍ 把 JSON 文件塞进 CJAVal 对象的坑

在 MT5 里读外部 JSON 不是直接拿字符串就完事。上面这段把文件流拼成 sStream 后,必须用 CJAVal 的 Deserialize 做反序列化;一旦返回 false,基本是结构破损或编码不对,直接 Print 出来能省你半小时盲调。 反序列化通过后别急着用,先查 status 字段。只有 ToStr() 等于 "ok" 才把 articles 节点赋给指针并返回 true,否则就是 API 侧拒绝访问,这种分支不处理会让后续数组越界崩 EA。 外汇与贵金属行情接口常返回类似结构,这类解析逻辑建议单独封装;实盘前在策略测试器里丢一份样本 json 跑一遍,看 Print 日志有没有「Json 析构错误」或「错误 json api 访问禁止」。

MQL5 / C++
    sStream    += ::FileReadString(hFile, iStringSize);
    }
  ::FileClose(hFile);
  }
class=class="str">"cmt">// 解析 json 文件
  if(!js.Deserialize(sStream))
   {
    ::Print("Json 析构错误");
    class="kw">return(class="kw">false);
   }
class=class="str">"cmt">// 将 json 对象分配给 articles 指针
  if(js["status"].ToStr()=="ok")
   {
    articles=js["articles"];
    class="kw">return(true);
   }
  else
   {
    Print("错误 json api 访问禁止");
    class="kw">return(class="kw">false);
   }
  }

点击列表后文本框的刷新逻辑

在 CNewsFeed 类里,图表事件处理器 OnEvent 负责把用户点击列表视图的动作转成新闻标题刷新。核心动作是先让文本框垂直滚到 0 位置,避免旧滚动偏移把新内容顶出可视区;这一步只在 m_text_box.GetScrollVPointer().IsScroll() 为真时才执行。 拿到点击项靠两个方法:SelectedItemText() 给渠道名,SelectedItemIndex() 给它在 sources 数组的下标。把下标喂给 PrepareArticles() 拼出正确 API URL,成功返回 true 且 articles 数组长度大于 0,就清空文本框、写一行「渠道名 Top HeadLines:」再逐条塞标题。 失败分支里代码把 asize 硬设为 1,显然是为后续统一走“只显示一行错误提示”的渲染路径留的兜底。整套类定义到此结束,直接 #include 进 EA 就能用。 别把滚动复位当可有可无 很多自行改写的面板漏了 VerticalScrolling(0),结果点第 5 个源回来点第 1 个,标题从半屏开始画,看起来像“没刷新”。MT5 里拖一下滚动条就能复现这个坑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 图标事件处理器                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CNewsFeed::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">// 如果选择了其中一个新闻资源 (点击)
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      class="type">class="kw">string nameofobject="";                    class=class="str">"cmt">//所选新闻来源的名称
      class="type">int selected,asize,g;                      class=class="str">"cmt">//selected - 选定列表条目的索引 , asize - articles JSON 对象数组的大小, g -数组索引
      class="type">bool ar=class="kw">false;                             class=class="str">"cmt">// ar - prepareArticles 方法的返回值
                                            class=class="str">"cmt">// 只在需要垂直滚动的情况下, 才可以首先将文本框自动滚至开头
      if(m_text_box.GetScrollVPointer().IsScroll())
         m_text_box.VerticalScrolling(class="num">0);
      class=class="str">"cmt">//---
      nameofobject=m_listview.SelectedItemText();
      class=class="str">"cmt">//---
      selected=m_listview.SelectedItemIndex();
      class=class="str">"cmt">//--- 
      ar=PrepareArticles(selected);
      class=class="str">"cmt">//---
      asize=(articles!=NULL)? ::ArraySize(articles.m_e):class="num">0;
      class=class="str">"cmt">//---删除文本框的当前内容
      m_text_box.ClearTextBox();
      class=class="str">"cmt">//--- 为新的文本框内容添加标题
      m_text_box.AddText(class="num">0,nameofobject+" Top HeadLines:");
      class=class="str">"cmt">//--- 取决于 PrepareArticles 方法是否填充文本框内容成功
      if(asize>class="num">0 && ar)class=class="str">"cmt">// 如果 PrepareArticles 成功
        {
         class="type">class="kw">string descrip,des;
         for(g=class="num">0; g<asize;g++)
           {
            class=class="str">"cmt">// 将 json 对象设置为 json 对象的 articles 数组的元素
            articlesArrayElement=articles[g];
            class=class="str">"cmt">// 获取数值 
            des=articlesArrayElement["description"].ToStr();
            class=class="str">"cmt">// 根据其可用性设置要显示的附加文本
            descrip=(des!="null" && des!="")? " -> "+des:".";
            class=class="str">"cmt">// 向文本框添加新的文本 
            m_text_box.AddLine(class="type">class="kw">string(g+class="num">1)+". "+articlesArrayElement["title"].ToStr()+descrip);
           }
        }
      else class=class="str">"cmt">// 如果 PrepareArticles 未成功 
        {
         asize=class="num">1; class=class="str">"cmt">// 设置 asize 为一 

「递送数据检索失败时的文本框回写」

在 MT5 的 EA 或指标里处理新闻递送对象时,若循环遍历 articles 数组出现越界或空指针,就要在图形文本框上给出明确提示而不是静默崩溃。 下面这段片段用 for 循环遍历 asize 个元素,一旦 g 达到 asize 仍未能取到合法数据,就向 m_text_box 追加一行中文报错,方便交易者在图表上直接看到故障点。 循环结束后必须调用 m_text_box.Update(true) 强制重绘,否则新增的报错行不会即时显示;随后把 articles 置为 NULL 释放引用,最后 return 退出事件处理函数,避免后续逻辑在脏数据上继续跑。

MQL5 / C++
    for(g=class="num">0; g<asize; g++)
      {
        class=class="str">"cmt">// 在文本框上显示错误消息
        m_text_box.AddLine("检索递送数据错误。");
      }
    }
    class=class="str">"cmt">//-- 重绘文本框    
    m_text_box.Update(true);
    class=class="str">"cmt">//-- 重置 articles 对象的值
    articles=NULL;
    class=class="str">"cmt">//Print("点击的列表条目是 "+nameofobject);
    class="kw">return;
  }
}

◍ 把新闻流挂进EA主循环

想在MT5里跑一个只做新闻展示、不直接下单的EA,核心是把网络请求和界面逻辑塞进一个独立的类,主文件只负责生命周期转发。上面这段就是这类EA的骨架:用 #include 拉进 NewsFeedProgram.mqh,声明一个 CNewsFeed program 实例,所有脏活都在类里。 OnInit 里先调 program.OnInitEvent(),失败就弹 Alert 提示检查互联网连接和终端的 WebRequest 权限设置,并返回 INIT_FAILED;接着 program.CreateGUI() 建面板,再失败就 Print 并返回 INIT_FAILED。两个检查都过才返回 INIT_SUCCEEDED,EA才算挂上图表。 OnTick 是空的——这个EA不处理报价,只靠 OnTimer 驱动。定时器触发后调 program.OnTimerEvent() 去拉新闻、刷新界面;OnDeinit 里调 program.OnDeinitEvent(reason) 做清理。整套结构把‘网络+GUI’和‘EA事件框架’解耦,你改新闻源或面板样式不用动主流程。 实盘前记得在MT5选项里把EA用的域名加进‘允许WebRequest的URL列表’,否则 OnInitEvent 必失败。外汇和贵金属受新闻跳空影响大,这类工具只帮你看,不替你管风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 NewsFeedExpert.mq5 |
class=class="str">"cmt">//|                                                                 版权所有 class="num">2017, ufranco |
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2017, ufranco"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <NewsFeedProgram.mqh>
CNewsFeed program;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   if(!program.OnInitEvent())
     {
      ::Alert("检查您的互联网连接并设置终端 \n"+
              "用来进行 Web 请求");
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- 设置交易面板
   if(!program.CreateGUI())
     {
      ::Print("创建图形界面失败!");
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- 初始化成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逆初始化函数                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- 摧毁计时器
   program.OnDeinitEvent(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逐笔报价函数                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定时器函数                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
  {
class=class="str">"cmt">//---
   program.OnTimerEvent();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent 函数                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+

把图表交互转交给程序对象

想在 MT5 图表上捕捉鼠标点击、键盘输入或自定义事件,核心入口是 OnChartEvent 回调。它把系统抛来的事件参数原样接住,再转交给你的程序对象去处理。 下面这段代码演示了最精简的转发写法:声明好四个入参,函数体内只调用 program.ChartEvent 把 id、lparam、dparam、sparam 传进去。这样事件逻辑可以集中在程序类里写,不必散落在全局函数。 外汇与贵金属市场波动剧烈、杠杆风险高,这类交互脚本仅用于辅助观察,不构成任何方向暗示。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id,
                     class="kw">const class="type">long &lparam,
                     class="kw">const class="type">class="kw">double &dparam,
                     class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//---
   program.ChartEvent(id,lparam,dparam,sparam);
  }

「让 EA 跑起来的最后一步」

前面演示的新闻推送 EA 只是骨架,真正要它在 MT5 里活起来,还差一个硬依赖:GUI 函数库。没它,TextBox 控件根本编译不过。 具体操作就两步——先去把 GUI 函数库整包下载到本地 Include 目录;再把压缩包里附带的 TextBox.mqh 覆盖掉函数库原文件。这个替换不是可选项,原文明确点出「为了令 EA 正常工作」必须这么做。 外汇与贵金属行情受新闻冲击剧烈,这类自定义推送工具能帮你抢几分钟先手,但信号本身不预示方向,杠杆品种依旧高风险,实盘前请在策略测试器里空跑确认新闻拉取频率。

◍ 随包附带的代码文件清单

这套新闻递送方案不是纯理论,作者把可运行的源码都打进了 ZIP,总共 8 个文件,合计约 330 KB。其中 NewsFeedProgram.mqh 占 35.64 KB,TextBox.mqh 最大,有 282.87 KB,后者改写了标准文本框类,专门用来在图表上渲染格式化新闻流。 JAson.mqh 与 json.mqh 是两个头文件,分别负责 JSON 的序列化/反序列化和解析;NewsFeedExpert.mq5 是挂 EA 的主入口,NewsAPI_test.mq5 以及 newstest_JAson.mq5、newstest_json.mq5 都是验证接口和函数库用的脚本。 想在 MT5 里跑通,直接把 ZIP 解压进 MQL5 目录对应子文件夹,先编译 newstest_json.mq5 确认解析类没问题,再上 NewsFeedExpert.mq5。外汇与贵金属行情受新闻冲击大,这类自动递送仅作信息辅助,实盘决策仍带高风险,信号延迟或解析异常都可能让你误判。

让小布替你跑这套
这些诊断与新闻源接入的逻辑,小布盯盘的 AIGC 已内置,打开对应品种页即可看到聚合快讯,不必自己从头写 EA。

常见问题

可封装一个处理 Json 对象的方法组,用 CJAVal 或自行写的解析函数把标题与摘要提出来,避免每次请求都重写遍历逻辑。
CNewsFeed 负责请求与数据持有,CTextBox 改造后专注渲染刷新,两者通过刷新文本框接口解耦,便于后期换 UI。
倾向放在独立定时器或低频 OnTimer 中,控制请求间隔,避免 WebRequest 超时拖慢主线程导致成交延迟。
目前小布内置的是通用聚合源;若你有私有 API 密钥与白名单,可反馈给知识库,后续版本可能开放自定义源接入。
不限定会拉回全部媒体列表,既费额度又让下拉框冗长,按类别筛能精准匹配你的交易品种相关新闻。