让新闻交易轻松上手(第4部分):性能增强·进阶篇

让新闻交易轻松上手(第4部分):性能增强·进阶篇

(2/3)·回测时频繁查内存库让 EA 慢到没法忍?本篇给出降访问次数的底层模块

案例拆解新手友好 第 2/3 篇

接上篇,我们继续深挖新闻交易 EA 的工程实现。上一版回测能跑通,但每根 K 线都去翻内存里的事件库,事件密集的日子直接卡成幻灯片。这一篇先把时间变量类和按小时聚类的骨架搭好,让后续调用只碰一次库。

◍ 把整数安全塞进时间枚举

在 MT5 里做分钟级或秒级触发逻辑时,直接用裸整数当枚举值很容易越界。CTimeVariables 类用两个小函数把 uint 收口:分钟超过 59 就归零,秒超过 59 也归零,返回对应的 MINUTELY / SECONDLY 枚举。 小时维度同理,Hourly() 对大于 23 的输入回退到 HOURLY(0),也就是午夜 00 点。HOURLY 枚举里 H24 被显式赋值为 0,和 H1~H23 形成 1–23 加 0 的闭环,避免 24 这个不存在的钟点钻进条件判断。 下面这段是可直接贴进 EA 的收口实现,注意枚举定义与越界兜底都在同一层: enum HOURLY { H1=1,H2=2,H3=3,H4=4,H5=5,H6=6,H7=7,H8=8,H9=9,H10=10,H11=11,H12=12,H13=13,H14=14,H15=15,H16=16,H17=17,H18=18,H19=19,H20=20,H21=21,H22=22,H23=23,H24=0 }; HOURLY CTimeVariables::Hourly(uint Hour) { return (Hour>23)?HOURLY(0):HOURLY(Hour); } MINUTELY CTimeVariables::Minutely(uint Minute) { return (Minute>59)?MINUTELY(0):MINUTELY(Minute); } SECONDLY CTimeVariables::Secondly(uint Second) { return (Second>59)?SECONDLY(0):SECONDLY(Second); } 开盘前把 current_hour 设成 H10,跑 if(current_hour==H10) Print("The current hour is 10:00 AM"); 能在策略测试器日志里确认枚举比较生效。外汇与贵金属波动受时段影响明显,这类时间锚点仅用于触发,不代表任何方向概率。

MQL5 / C++
class=class="str">"cmt">//|Convert Integer to the Enumeration MINUTELY                                                                 |//+------------------------------------------------------------------+
MINUTELY CTimeVariables::Minutely(class="type">uint Minute)
  {
   class="kw">return (Minute>class="num">59)?MINUTELY(class="num">0):MINUTELY(Minute);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Convert Integer to the Enumeration SECONDLY                                                       |//+------------------------------------------------------------------+
SECONDLY CTimeVariables::Secondly(class="type">uint Second)
  {
   class="kw">return (Second>class="num">59)?SECONDLY(class="num">0):SECONDLY(Second);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
enum HOURLY
  {
   H1=class="num">1,   class=class="str">"cmt">// Represents Hour class="num">01
   H2=class="num">2,   class=class="str">"cmt">// Represents Hour class="num">02
   H3=class="num">3,   class=class="str">"cmt">// Represents Hour class="num">03
   H4=class="num">4,   class=class="str">"cmt">// Represents Hour class="num">04
   H5=class="num">5,   class=class="str">"cmt">// Represents Hour class="num">05
   H6=class="num">6,   class=class="str">"cmt">// Represents Hour class="num">06
   H7=class="num">7,   class=class="str">"cmt">// Represents Hour class="num">07
   H8=class="num">8,   class=class="str">"cmt">// Represents Hour class="num">08
   H9=class="num">9,   class=class="str">"cmt">// Represents Hour class="num">09
   H10=class="num">10, class=class="str">"cmt">// Represents Hour class="num">10
   H11=class="num">11, class=class="str">"cmt">// Represents Hour class="num">11
   H12=class="num">12, class=class="str">"cmt">// Represents Hour class="num">12
   H13=class="num">13, class=class="str">"cmt">// Represents Hour class="num">13
   H14=class="num">14, class=class="str">"cmt">// Represents Hour class="num">14
   H15=class="num">15, class=class="str">"cmt">// Represents Hour class="num">15
   H16=class="num">16, class=class="str">"cmt">// Represents Hour class="num">16
   H17=class="num">17, class=class="str">"cmt">// Represents Hour class="num">17
   H18=class="num">18, class=class="str">"cmt">// Represents Hour class="num">18
   H19=class="num">19, class=class="str">"cmt">// Represents Hour class="num">19
   H20=class="num">20, class=class="str">"cmt">// Represents Hour class="num">20
   H21=class="num">21, class=class="str">"cmt">// Represents Hour class="num">21
   H22=class="num">22, class=class="str">"cmt">// Represents Hour class="num">22
   H23=class="num">23, class=class="str">"cmt">// Represents Hour class="num">23
   H24=class="num">0    class=class="str">"cmt">// Represents Hour class="num">00 (Midnight)
   };
HOURLY current_hour = H10; class=class="str">"cmt">// Setting the current hour to class="num">10:class="num">00 AM
if (current_hour == H10)
{
   Print("The current hour is class="num">10:class="num">00 AM");
}
enum MINUTELY
{
   M0, M1, M2, M3, ..., M59
};
enum PRESECONDLY
{
   Pre_S30 = class="num">30 class=class="str">"cmt">// class="num">30 seconds before an event
};
enum SECONDLY
{
   S0, S1, S2, ..., S59
};
HOURLY CTimeVariables::Hourly(class="type">uint Hour)
{
   class="kw">return (Hour>class="num">23)?HOURLY(class="num">0):HOURLY(Hour);
}
class="kw">return (Hour>class="num">23)?HOURLY(class="num">0):HOURLY(Hour);
MINUTELY CTimeVariables::Minutely(class="type">uint Minute)
{
   class="kw">return (Minute>class="num">59)?MINUTELY(class="num">0):MINUTELY(Minute);
}
SECONDLY CTimeVariables::Secondly(class="type">uint Second)
{
   class="kw">return (Second>class="num">59)?SECONDLY(class="num">0):SECONDLY(Second);
}

「按小时切分事件数组省掉无效查询」

EA 在新闻驱动策略里常把当天事件先塞进内存结构体数组,再靠 tick 触发去检索下一事件。若一天内多个事件时间分散,每次新 tick 都从头遍历全量数组,CPU 被无意义的日期比对吃掉。 典型浪费发生在「当前 11:00,却反复检查 14:30 是否匹配」这类场景——事件遥不可及,数组却被每 tick 扫一遍。原文给出的解法:把全天事件按小时拆成独立数组,EA 只进与当前小时对应的那段做匹配。 以美国 NFP 为例,报告固定在 14:30 发布。未优化时 EA 整日每秒查库,到 14:30 可能因查询堆积而延迟挂单;按小时分段后,00:00 载入全天,临近 14:00 才切进 14 点数组,14:30 到达即响应,不再有库访问阻塞。外汇与贵金属新闻行情滑点大、风险高,这种延迟可能直接错过止损单最佳挂单价。 下面这段是某小时数组的实测时间样本,可见 14:00–15:55 被细分为 13 个节点,EA 只需在该小时区间内线性检索:

MQL5 / C++
time[class="num">0] = &class="macro">#x27;class="num">14:class="num">00&class="macro">#x27;
time[class="num">1] = &class="macro">#x27;class="num">14:class="num">15&class="macro">#x27;
time[class="num">2] = &class="macro">#x27;class="num">14:class="num">30&class="macro">#x27;
time[class="num">3] = &class="macro">#x27;class="num">14:class="num">45&class="macro">#x27;
time[class="num">4] = &class="macro">#x27;class="num">14:class="num">50&class="macro">#x27;
time[class="num">5] = &class="macro">#x27;class="num">14:class="num">55&class="macro">#x27;
time[class="num">6] = &class="macro">#x27;class="num">15:class="num">00&class="macro">#x27;
time[class="num">7] = &class="macro">#x27;class="num">15:class="num">05&class="macro">#x27;
time[class="num">8] = &class="macro">#x27;class="num">15:class="num">10&class="macro">#x27;
time[class="num">9] = &class="macro">#x27;class="num">15:class="num">15&class="macro">#x27;
time[class="num">10] = &class="macro">#x27;class="num">15:class="num">30&class="macro">#x27;
time[class="num">11] = &class="macro">#x27;class="num">15:class="num">45&class="macro">#x27;
time[class="num">12] = &class="macro">#x27;class="num">15:class="num">55&class="macro">#x27;

把一天拆成 24 个独立小时容器

做日内事件映射时,最怕把不同小时的数据混在一个扁平数组里,回头按小时回看就得多写一层过滤。CTimeByHour 的思路很直接:给 00–23 点各建一个 CArrayObj 指针(myH1…myH24),一共 24 个对象容器,写入时按 HOURLY 枚举分流,读取时 switch 一把就能拿到对应小时的独立数组。 底层用了两个并行数组来存单条记录:TimeDate 结构体只装 Hour 和 Minute 两个 int 字段,myTimeData[] 与之同索引的 myEvents[] 装 Calendar 事件体。这样时间和事件严格一一对应,不会在插入新数据后错位。 构造函数是带参的,Hour(int(myHour))、Minute(int(myMinute)) 直接由枚举强转赋值,同时把外部传入的 Calendar &myEventData 绑给成员 myEData。私有里还挂了 CTimeManagement 和 CTimeVariables 两个自定义类对象,说明时间解析和变量缓存交给下属类,本类只管按小时收纳。 析构函数里挨个 delete 掉 myH1…myH24 以及那两个类指针,动态内存不拖泥带水。GetDataForHour 方法先用 getTime(myHour) 取回该小时的 CArrayObj,再 for 循环遍历、用 ArrayResize 扩 TimeData[] 和 Events[],把每条的 Hour/Minute/myEData 填进对应索引——开 MT5 把这段 include 进 EA,断点跟一遍就能看清每小时对象是怎么被填充的。外汇与贵金属行情受时段流动性影响大,这类按小时归集的做法仅降低数据管理复杂度,不预示任何方向。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TimeByHour.mqh |
class=class="str">"cmt">//|                                                     Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include <Arrays\ArrayObj.mqh>
class="macro">#include "../TimeVariables.mqh"
class="macro">#include "../CommonVariables.mqh"
class="macro">#include "../TimeManagement.mqh"
class=class="str">"cmt">//--- Structure to store time data in Hour and Minute
class="kw">struct TimeDate
  {
   class="type">int               Hour;
   class="type">int               Minute;
  } myTimeData[];
class=class="str">"cmt">//--- Structure array to store event data in parallel with myTimeData array
Calendar myEvents[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|TimeByHour class                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTimeByHour:class="kw">public CObject
  {
class="kw">private:
   class=class="str">"cmt">//--- classes&class="macro">#x27; object declarations
   CTimeManagement   CTime;
   CTimeVariables    CTV;
class="kw">protected:
   class=class="str">"cmt">//--- class constructor with parameters
                     CTimeByHour(HOURLY myHour,MINUTELY myMinute,Calendar &myEventData):
   class=class="str">"cmt">//--- Assign integer variables Hour and Minute with time data from myHour and myMinute respectively
                     Hour(class="type">int(myHour)),Minute(class="type">int(myMinute))
   {
   class=class="str">"cmt">//--- Assign variable myEData with event info from variable myEventData

◍ 按小时槽位拆解财经日历事件

把一天切成 24 个独立槽位,是做时段统计最干净的做法。下面这个类用 myH1 到 myH24 共 24 个 CArrayObj 指针,分别挂住对应小时内的日历事件对象,避免把所有新闻混在一个大数组里翻找。 构造函数 CTimeByHour(void) 是空的,真正的初始化放在别处;getTime(HOURLY myHour) 用 switch 把 H1~H24 映射到各自的指针,默认分支落回 myH24(即 0 点 / 24 点)。这样你在 EA 里写 getTime(H8) 就能直接拿到早上 8 点那一组事件,不用遍历全量数据。 析构里逐个 delete:getmyTime、myClass、myH1…myH24 都要释放。漏掉任意一个,MT5 跑长周期回测时内存占用会缓慢爬升,可能触发终端强制回收。外汇与贵金属受新闻冲击跳空频繁,这类按小时归类的结构能帮你快速定位某时段事件密度,但事件驱动本身高风险,信号仅作概率参考。 GetDataForHour 通过引用传出某小时的 TimeDate[] 与 Calendar[],调用前先 Clean arrays,保证每次拿到的不是上一次残留。实盘前建议打印各小时数组大小,验证服务器推送的日历是否真的按槽位落位。

MQL5 / C++
myEData = myEventData;
  }
  class="kw">virtual class="type">void      myTime(Calendar &myNews[]);
  class=class="str">"cmt">//--- Array object declarations for each hour of the day
  CArrayObj        *myH1,*myH2,*myH3,...,*myH24;
  class=class="str">"cmt">//--- Integer variables to store time data in hour and minute format
  class="type">int              Hour;
  class="type">int              Minute;
  class=class="str">"cmt">//--- Calendar structure variable to store event info
  Calendar         myEData;
class="kw">public:
  class=class="str">"cmt">//--- class constructor without parameters
                   CTimeByHour(class="type">void)
  {
  }
  class=class="str">"cmt">//--- Array object variable
  CArrayObj        *getmyTime;
  class=class="str">"cmt">//--- Retrieve array object for an individual hour
  CObject          *getTime(HOURLY myHour)
  {
    class="kw">switch(myHour)
    {
     case  H1:
       class=class="str">"cmt">//--- retrieve array obj for class="num">01 Hour
       class="kw">return myH1;
       class="kw">break;
     case H2:
       class=class="str">"cmt">//--- retrieve array obj for class="num">02 Hour
       class="kw">return myH2;
       class="kw">break;
     case H3:
       class=class="str">"cmt">//--- retrieve array obj for class="num">03 Hour
       class="kw">return myH3;
       class="kw">break;
class=class="str">"cmt">// ...
     class="kw">default:
       class=class="str">"cmt">//--- retrieve array obj for class="num">24|class="num">00 Hour
       class="kw">return myH24;
       class="kw">break;
    }
  }
  class=class="str">"cmt">//--- class pointer variable
  CTimeByHour      *myClass;
  class=class="str">"cmt">//--- class destructor
                   ~CTimeByHour(class="type">void)
  {
   class=class="str">"cmt">//--- class="kw">delete all pointer variables
   class="kw">delete getmyTime;
   class="kw">delete myClass;
   class="kw">delete myH1;
   class="kw">delete myH2;
   class="kw">delete myH3;
class=class="str">"cmt">// ...
  }
  class=class="str">"cmt">//--- Function to retrieve timedata and calendar info for a specific hour of the day via parameters passed by reference
  class="type">void             GetDataForHour(HOURLY myHour,TimeDate &TimeData[],Calendar &Events[])
  {
   class=class="str">"cmt">//--- Clean arrays

「按小时抽取财经事件到数组」

把日历事件按小时归类后,真正要用时还得落进普通数组方便策略读取。下面这段逻辑先清空目标数组,再按指定小时从对象容器里把数据搬出来。 清空用 ArrayRemove 配 WHOLE_ARRAY,比手动循环置空更省事;getTime(myHour) 按小时枚举返回对应的 CArrayObj 指针,H1 到 H24 各自独立。 循环里每取一个 CTimeByHour 实例就 ArrayResize 一次,TimeData 和 Events 同步扩容,再把 Hour、Minute 和 myEData 写进下标 i。MT5 里这种「对象容器→结构体数组」的搬运,回测时若某小时有 30 条事件,数组就会扩到 30 长度。 外汇与贵金属受事件驱动跳空频繁,这类按小时归集的数据只描述历史分布,不预示下一根 K 线方向,实盘使用前建议在策略测试器用 2023 年全年数据跑一遍验证命中率。

MQL5 / C++
class="type">void GetDataForHour(HOURLY myHour, TimeDate &TimeData[], Calendar &Events[])
{
  ArrayRemove(TimeData, class="num">0, WHOLE_ARRAY);
  ArrayRemove(Events, class="num">0, WHOLE_ARRAY);
  getmyTime = getTime(myHour);
  for(class="type">int i = class="num">0; i < getmyTime.Total(); i++)
  {
    myClass = getmyTime.At(i);
    ArrayResize(TimeData, i + class="num">1);
    ArrayResize(Events, i + class="num">1);
    TimeData[i].Hour = myClass.Hour;
    TimeData[i].Minute = myClass.Minute;
    Events[i] = myClass.myEData;
  }
}
把重复劳动交给小布
这些时间枚举与小时聚类的诊断逻辑,小布盯盘已内置到对应品种页,打开即可看到事件分布密度,不必自己手写 switch 查表。

常见问题

回测中若每 tick 都访问内存数据库读取新闻,事件多时访问次数呈量级膨胀;改为按日缓存、按小时聚类后,访问可能降至每天一次,运行时间倾向显著缩短。
可以,小布盯盘对应品种页内置了事件按小时分布的视图,省去你自己写 HOURLY 枚举和 switch 查表的过程,你专注决策即可。
午夜在系统时间里是 00:00,对应整数 0;把 H24 显式映射到 0 是为了代码里用命名常量处理跨日边界,避免 raw 数字误用。
取决于你策略的预警窗口,常见为事件前 0–300 秒分段;具体粒度见同系列实战篇,本篇只定义可控的秒级枚举基底。