DoEasy 函数库中的时间序列(第六十一部分):品种即时报价序列集合·综合运用
📘

DoEasy 函数库中的时间序列(第六十一部分):品种即时报价序列集合·综合运用

第 3/3 篇

「引擎里多品种时间序列的挂载入口」

在自建交易引擎里,多品种数据的初始化往往卡在「符号集—时序集— tick 集」三者的绑定顺序。下面这段实现把外部传入的符号数组直接交给底层符号管理器,再借已用符号列表一次性拉起 K 线与 tick 两条采集通道。 SetUsedSymbols 的返回值是逐层按位与出来的:先判符号写入成败,再判时序集合创建,最后那行高亮的 m_tick_series.CreateCollection(list,required) 才是 tick 流真正开户的地方;required 参数为 0 时表示不强制最少 tick 数,实盘里若设成 1000 则可能过滤掉刚上架流动性不足的交叉盘。 SeriesCopyToBufferAsSeries 把任意品种任意周期的双精度 BAR 属性(如收盘价、点差)按时间序列方向拷进数组,不受调用方数组下标方向干扰,回测和实盘用同一套取数逻辑能少踩不少坑。 GetTickSeriesCollection 与 GetListTickSeries 两个 getter 暴露了 tick 集合本体和内部对象列表,想在 EA 里遍历实时 tick 做高频统计,直接从这两个口接就行,不必再绕一层符号查询。

MQL5 / C++
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 各一条。外汇和贵金属杠杆高,多品种常驻序列会吃内存和初始化时间,上真仓前先在策略测试器跑一遍。

MQL5 / C++
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 序列,省去逐个绑定。

MQL5 / C++
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 参数还能顺手统计其他字段的极端值。

MQL5 / C++
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 对照市场报价窗口即可验证。

MQL5 / C++
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 越界或序列为空时它可能直接崩编辑器:

MQL5 / C++
class="num">237        engine.GetTickSeriesCollection().GetTick(tick_series.Symbol(),index_max).Print();
class="num">238        engine.GetTickSeriesCollection().GetTick(tick_series.Symbol(),index_min).Print();

常见问题

在引擎初始化时把品种名数组传给 CTickSeries 集合类,EA只留调用接口,具体挂载与更新由函数库完成,降低主程序耦合。
在每笔 tick 回调里调用集合的更新方法,再用 GetTickHigh/GetTickLow 取当日极值,打印即可看到真实落点。
小布可自动加载对应品种页,把多品种当日高低 tick 与零成交量现象做 AIGC 诊断,你直接看结论不用自己跑打印。
属正常报价机制,零成交量 tick 多为报价刷新而非真实成交,做量价分析时需剔除这类 tick 避免误判。
不会,函数库按各自时间戳独立存,H1 取自整点聚合、tick 取逐笔,打印同一时刻两序列可验证一致性。