让新闻交易轻松上手(第4部分):性能增强·进阶篇
(2/3)·回测时频繁查内存库让 EA 慢到没法忍?本篇给出降访问次数的底层模块
接上篇,我们继续深挖新闻交易 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"); 能在策略测试器日志里确认枚举比较生效。外汇与贵金属波动受时段影响明显,这类时间锚点仅用于触发,不代表任何方向概率。
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 只需在该小时区间内线性检索:
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,断点跟一遍就能看清每小时对象是怎么被填充的。外汇与贵金属行情受时段流动性影响大,这类按小时归集的做法仅降低数据管理复杂度,不预示任何方向。
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,保证每次拿到的不是上一次残留。实盘前建议打印各小时数组大小,验证服务器推送的日历是否真的按槽位落位。
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 年全年数据跑一遍验证命中率。
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; } }