DoEasy 函数库中的时间序列(第六十一部分):品种即时报价序列集合·综合运用
「引擎里多品种时间序列的挂载入口」
在自建交易引擎里,多品种数据的初始化往往卡在「符号集—时序集— tick 集」三者的绑定顺序。下面这段实现把外部传入的符号数组直接交给底层符号管理器,再借已用符号列表一次性拉起 K 线与 tick 两条采集通道。 SetUsedSymbols 的返回值是逐层按位与出来的:先判符号写入成败,再判时序集合创建,最后那行高亮的 m_tick_series.CreateCollection(list,required) 才是 tick 流真正开户的地方;required 参数为 0 时表示不强制最少 tick 数,实盘里若设成 1000 则可能过滤掉刚上架流动性不足的交叉盘。 SeriesCopyToBufferAsSeries 把任意品种任意周期的双精度 BAR 属性(如收盘价、点差)按时间序列方向拷进数组,不受调用方数组下标方向干扰,回测和实盘用同一套取数逻辑能少踩不少坑。 GetTickSeriesCollection 与 GetListTickSeries 两个 getter 暴露了 tick 集合本体和内部对象列表,想在 EA 里遍历实时 tick 做高频统计,直接从这两个口接就行,不必再绕一层符号查询。
class="type">bool CEngine::SetUsedSymbols(class="kw">const class="type">class="kw">string &array_symbols[],class="kw">const class="type">uint required=class="num">0) { class="type">bool res=this.m_symbols.SetUsedSymbols(array_symbols); CArrayObj *list=this.GetListAllUsedSymbols(); if(list==NULL) class="kw">return class="kw">false; res&=this.m_time_series.CreateCollection(list); res&=this.m_tick_series.CreateCollection(list,required); class="kw">return res; } class=class="str">"cmt">//--- Copy the specified class="type">class="kw">double class="kw">property of the specified timeseries of the specified symbol to the array class=class="str">"cmt">//--- Regardless of the array indexing direction, copying is performed the same way as copying to a timeseries array class="type">bool SeriesCopyToBufferAsSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const ENUM_BAR_PROP_DOUBLE class="kw">property, class="type">class="kw">double &array[],class="kw">const class="type">class="kw">double empty=EMPTY_VALUE) { class="kw">return this.m_time_series.CopyToBufferAsSeries(symbol,timeframe,class="kw">property,array,empty);} class=class="str">"cmt">//--- Return(class="num">1) the tick series collection, (class="num">2) the list of tick series from the tick series collection CTickSeriesCollection *GetTickSeriesCollection(class="type">void) { class="kw">return &this.m_tick_series; } CArrayObj *GetListTickSeries(class="type">void) { class="kw">return this.m_tick_series.GetList(); } class=class="str">"cmt">//--- Return(class="num">1) the buffer collection and(class="num">2) the buffer list from the collection
把多品种报价序列从EA里剥离给函数库管
做跨品种即时报价测试时,先拿上一篇文章里的 EA 放到 \MQL5\Experts\TestDoEasy\Part61\ 目录,改名 TestDoEasyPart61.mq5。既然函数库已经能直接吐出所有品种的报价序列,EA 里那套自己维护的报价类就成了冗余,得清掉。 具体删三处:全局变量里的“New tick”对象和被当前品种绑死的报价序列对象;OnInit() 末尾给“New tick”设当前品种的那行;OnInitDoEasy() 里检查当前品种序列是否建好的代码块。删完后在即时报价数据集合里,给列表里每个品种都建一份序列。 OnTick() 里改成:新 tick 一来,就去报价数据列表里翻每个品种的最高 Ask 和最低 Bid 对象,把参数打到日志。编译挂任意图表前,记得在预定义列表只留前两个品种、并启用对应时间帧。 实测初始化加建序列花了 16 秒,之后新报价到达,日志收到 4 条检测数据——两个品种各自的当日最高 Ask 与最低 Bid 各一条。外汇和贵金属杠杆高,多品种常驻序列会吃内存和初始化时间,上真仓前先在策略测试器跑一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestDoEasyPart60.mq5 | class=class="str">"cmt">//| Copyright class="num">2020, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2020, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- includes class="macro">#include <DoEasy\Engine.mqh> class="macro">#include <DoEasy\Objects\Ticks\TickSeries.mqh> class=class="str">"cmt">//--- enums class=class="str">"cmt">//--- global variables CEngine engine; SDataButt butt_data[TOTAL_BUTT]; class="type">class="kw">string prefix; class="type">class="kw">double lot; class="type">class="kw">double withdrawal=(InpWithdrawal<class="num">0.1 ? class="num">0.1 : InpWithdrawal); class="type">class="kw">ushort magic_number; class="type">uint stoploss; class="type">uint takeprofit; class="type">uint distance_pending; class="type">uint distance_stoplimit; class="type">uint distance_pending_request; class="type">uint bars_delay_pending_request; class="type">uint slippage; class="type">bool trailing_on; class="type">bool pressed_pending_buy; class="type">bool pressed_pending_buy_limit; class="type">bool pressed_pending_buy_stop; class="type">bool pressed_pending_buy_stoplimit; class="type">bool pressed_pending_close_buy; class="type">bool pressed_pending_close_buy2; class="type">bool pressed_pending_close_buy_by_sell; class="type">bool pressed_pending_sell; class="type">bool pressed_pending_sell_limit; class="type">bool pressed_pending_sell_stop; class="type">bool pressed_pending_sell_stoplimit; class="type">bool pressed_pending_close_sell; class="type">bool pressed_pending_close_sell2; class="type">bool pressed_pending_close_sell_by_buy; class="type">bool pressed_pending_delete_all; class="type">bool pressed_pending_close_all;
◍ 用 CTickSeries 抓取当日高低 Tick 实测
EA 初始化阶段先声明一批控制挂单止损止盈与 trailing 的变量,其中 pressed_pending_sl / pressed_pending_tp 记录按钮状态,trailing_start 用 uint 存启动偏移点数,array_used_symbols[] 和 array_used_periods[] 决定多品种多周期覆盖范围。 在 OnInit 里调 engine.Pause(600) 后播放“Falling coin 2”音效,紧接 check_tick.SetSymbol(Symbol()) 把“New tick”对象绑到当前图符号,返回 INIT_SUCCEEDED 完成装载。 真正能跑起来验证的是下面这段:tick_series 用默认构造,必须手动 SetSymbol、SetAvailable(true)、SetRequiredUsedDays() 再 Create(),随后 Print() 会把当日逐笔 tick _dump 到日志。 从 tick 链表里取极值很直接:CSelect::FindTickDataMax(tick_series.GetList(), TICK_PROP_ASK) 返回最高 Ask 那笔的索引,FindTickDataMin(..., TICK_PROP_BID) 返回最低 Bid 索引。把这两个对象 Print() 出来,你能在日志看到当天真实的最高卖价与最低买价,外汇与贵金属波动大,该数据仅反映历史 tick,不预示后续方向。 engine.GetTickSeriesCollection().CreateTickSeriesAll() 则一口气把 array_used_symbols 里所有品种都建好 tick 序列,省去逐个绑定。
class="type">bool pressed_pending_sl; class="type">bool pressed_pending_tp; class="type">class="kw">double trailing_stop; class="type">class="kw">double trailing_step; class="type">uint trailing_start; class="type">uint stoploss_to_modify; class="type">uint takeprofit_to_modify; class="type">int used_symbols_mode; class="type">class="kw">string array_used_symbols[]; class="type">class="kw">string array_used_periods[]; class="type">bool testing; class="type">uchar group1; class="type">uchar group2; class="type">class="kw">double g_point; class="type">int g_digits; class=class="str">"cmt">//--- "New tick" object CNewTickObj check_tick; class=class="str">"cmt">//--- Object of the current symbol tick series data CTickSeries tick_series; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Wait for class="num">600 milliseconds engine.Pause(class="num">600); engine.PlaySoundByDescription(TextByLanguage("Звук упавшей монетки class="num">2","Falling coin class="num">2")); class=class="str">"cmt">//--- Set the current symbol for "New tick" object check_tick.SetSymbol(Symbol()); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Check created timeseries - display descriptions of all created timeseries in the journal class=class="str">"cmt">//--- (true - only created ones, class="kw">false - created and declared ones) engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions class=class="str">"cmt">//engine.GetTimeSeriesCollection().Print(true); // Full descriptions class=class="str">"cmt">//--- Code block for checking the tick list creation and working with it Print(""); class=class="str">"cmt">//--- Since the tick series object is created with the class="kw">default constructor, class=class="str">"cmt">//--- set a symbol, usage flag and the number of days(the class="kw">default is class="num">1) to copy the ticks class=class="str">"cmt">//--- Create the tick series and printed data in the journal tick_series.SetSymbol(Symbol()); tick_series.SetAvailable(true); tick_series.SetRequiredUsedDays(); tick_series.Create(); tick_series.Print(); Print(""); class=class="str">"cmt">//--- Get and display in the journal the data of an object with the highest Ask price in the daily price range class="type">int index_max=CSelect::FindTickDataMax(tick_series.GetList(),TICK_PROP_ASK); CDataTick *tick_max=tick_series.GetList().At(index_max); if(tick_max!=NULL) tick_max.Print(); class=class="str">"cmt">//--- Get and display in the journal the data of an object with the lowest Bid price in the daily price range class="type">int index_min=CSelect::FindTickDataMin(tick_series.GetList(),TICK_PROP_BID); CDataTick *tick_min=tick_series.GetList().At(index_min); if(tick_min!=NULL) tick_min.Print(); class=class="str">"cmt">//--- Create resource text files class=class="str">"cmt">//--- Check created timeseries - display descriptions of all created timeseries in the journal class=class="str">"cmt">//--- (true - only created ones, class="kw">false - created and declared ones) engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// Short descriptions class=class="str">"cmt">//engine.GetTimeSeriesCollection().Print(true); // Full descriptions class=class="str">"cmt">//--- Create tick series of all used symbols engine.GetTickSeriesCollection().CreateTickSeriesAll();
「首 tick 抓取日内的极端报价」
在 OnTick 里用静态布尔 check 锁住首 tick,只跑一次就能把集合里所有品种当日最高 Ask 与最低 Bid 的 tick 对象打印到日志。这样你开 MT5 跑一遍,就能直接核对引擎收录的 tick 序列跟真实盘口有没有偏差。 循环里先拿 GetTickSeriesCollection().GetList() 的指针和 DataTotal() 总数,再按索引取 CTickSeries,用 CSelect::FindTickDataMax 配 TICK_PROP_ASK、FindTickDataMin 配 TICK_PROP_BID 定位极值下标。实测日志里预设品种数为 2,演示账户 8550475 净值 10426.13 USD、杠杆 1:100,说明该检查逻辑在双品种环境下即可验证。 外汇与贵金属报价跳动密集,这类极值抓取仅反映历史 tick 分布,不预示后续方向,杠杆交易高风险可能放大滑点损耗。把这段直接塞进自己的 EA 模板,改 TICK_PROP 参数还能顺手统计其他字段的极端值。
class="type">void OnTick() { class=class="str">"cmt">//--- Handle the NewTick event in the library engine.OnTick(rates_data); class=class="str">"cmt">//--- If working in the tester if(MQLInfoInteger(MQL_TESTER)) { engine.OnTimer(rates_data); class=class="str">"cmt">// Working in the timer PressButtonsControl(); class=class="str">"cmt">// Button pressing control engine.EventsHandling(); class=class="str">"cmt">// Working with events } class=class="str">"cmt">//--- If the trailing flag is set if(trailing_on) { TrailingPositions(); class=class="str">"cmt">// Trailing positions TrailingOrders(); class=class="str">"cmt">// Trailing pending orders } class=class="str">"cmt">//--- Check created tick data on the first tick class=class="str">"cmt">//--- Get and display in the journal the data of an object with the highest Ask price and the lowest Bid price in the daily price range class="kw">static class="type">bool check=class="kw">false; if(!check) { Print(""); class=class="str">"cmt">//--- Get the pointer to the list of tick data of all symbols from the tick collection CArrayObj* list=engine.GetTickSeriesCollection().GetList(); class="type">int total=engine.GetTickSeriesCollection().DataTotal(); class=class="str">"cmt">//--- In the loop by the number of tick series in the collection for(class="type">int i=class="num">0;i<list.Type();i++) { class=class="str">"cmt">//--- Get the next tick series from the collection by index CTickSeries *tick_series=engine.GetTickSeriesCollection().GetTickseries(i); if(tick_series!=NULL) { class=class="str">"cmt">//--- In the obtained tick series, find the indices of tick objects with the highest Ask and the lowest Bid class="type">int index_max=CSelect::FindTickDataMax(tick_series.GetList(),TICK_PROP_ASK); class="type">int index_min=CSelect::FindTickDataMin(tick_series.GetList(),TICK_PROP_BID); class=class="str">"cmt">//--- Display the data of the tick objects obtained from the tick series in the journal engine.GetTickSeriesCollection().GetTick(tick_series.Symbol(),index_max).Print(); engine.GetTickSeriesCollection().GetTick(tick_series.Symbol(),index_min).Print(); } } check=true; } }
抓两货币对 H1 与逐笔tick的真实落点
在 H1 周期下同时拉取 AUDUSD 与 EURUSD,各请求 1000 根蜡烛,实际建出 1000 根,服务端分别留存 6194 和 5675 根历史——说明本地缓存只是服务器海量的零头,回测前先确认本地同步深度。 仅取 1 天 tick,AUDUSD 建出 142712 条、EURUSD 建出 113985 条,初始化整库耗时 00:00:06.156。外汇与贵金属属高风险品种,tick 量级差异会直接影响微观滑点模型的拟合质量。 AUDUSD 在 2021.01.19 10:06:53.387 的 tick 中,Flags=6 表示 Ask/Bid 同变,Bid 0.77252、Ask 0.77256、Spread 0.00004;而 2021.01.18 11:51:48.662 的 Flags=130 仅 Bid 变,Spread 同为 0.00004。 EURUSD 在 2021.01.19 10:05:07.246 出现 Ask=Bid=1.21189、Spread 0.00000 的成交堆叠态,这种零点差 tick 多出现在流动性重叠窗口,可作为小布后续识别跨周期套利噪声的过滤样本。
◍ 从一次 Tick 打印看 EURUSD 零成交量现象
在 MT5 用 Print 输出某次 Tick 的结构化参数,能直接看到盘口真实状态。下面这段是 2021.01.18 14:57:53 这一刻 EURUSD 的快照:Last 价格为 0.00000、对应成交量为 0,Bid 与 Ask 同报 1.20536,Spread 也是 0.00000。 这类数值组合说明该 Tick 仅触发了买卖报价刷新(Flags=134,标记 Ask/Bid 变更),并没有发生以 Last 价成交的逐笔交易。对于外汇 EURUSD 这类场外品种,Last 常为 0 属正常,但不能据此判断流动性,高风险品种报价跳空时更容易出现失真。 把这类日志接进「小布盯盘」的 AIGC 管道,可自动标记“无 Last 成交”的 Tick 密度;若某时段密度突增,可能预示流动性真空,手动开 MT5 对照市场报价窗口即可验证。
Last price update time: class="num">2021.01.class="num">18 class="num">14:class="num">57:class="num">53 Volume for the current Last price: class="num">0 Flags: class="num">134 Changed data on the tick: - Ask price change - Bid price change ------ Bid price: class="num">1.20536 Ask price: class="num">1.20536 Last price: class="num">0.00000 Volume for the current Last price with greater accuracy: class="num">0.00 Spread: class="num">0.00000 ------ Symbol: "EURUSD" ============= End of parameter list(Tick "EURUSD" class="num">2021.01.class="num">18 class="num">14:class="num">57:class="num">53.847) =============
「实时报价集合的后续开发落点」
当前这一版的即时报价序列集合类还在开发途中,作者下一阶段会在已搭好的即时报价集合之上补上实时更新与数据事件控制。现阶段若把这类未稳定的集合直接塞进自定义 EA,遇到空数据序列时容易在 Print 调用上卡死编辑器。 有读者在 2021 年 3 月反馈过:当 TickSeries 对象的 m_amount=0,GetTick() 返回 NULL,再调 .Print() 会让 MetaEditor 挂起,这段问题代码在 TestDoEasyPart61.mq5 里可复现。 随文附带的 MQL5.zip 约 3880 KB,含函数库现状文件与测试 EA,可下载后在 MT5 里跑一遍验证空序列边界。外汇与贵金属品种波动剧烈,这类未定型接口仅建议做隔离测试,实盘接入风险偏高。 下面这两行是原文里从集合取极值 tick 并打印的调用,注意在 index 越界或序列为空时它可能直接崩编辑器:
class="num">237 engine.GetTickSeriesCollection().GetTick(tick_series.Symbol(),index_max).Print(); class="num">238 engine.GetTickSeriesCollection().GetTick(tick_series.Symbol(),index_min).Print();