DoEasy 即时报价序列实时更新:为市场深度操控铺路的基础架构(基础篇)
📊

DoEasy 即时报价序列实时更新:为市场深度操控铺路的基础架构(基础篇)

(1/3)· 默认 20 万根即时报价上限如何自动滚动淘汰旧数据,避免多品种内存爆仓

含代码示例实战向 第 1/3 篇
很多交易者以为库里的报价序列会自动跟着行情走,实则新 tick 不主动存就永远是死数据。多品种跑统计时内存悄悄吃满,EA 卡死才回头查哪里漏了。本篇先把实时写入和上限淘汰的机制钉死,后面才有资格谈 DOM。

「把即时报价序列改成实时刷新」

DoEasy 函数库这一版动了报价序列的底层逻辑:原来靠定时轮询拿 Tick,现在改成事件驱动,新报价一到就直接写进序列容器,延迟从「下一根 K 线才看到」压缩到「当前 Tick 即可用」。对做 scalping 或盯盘的人来说,这意味着你在 MT5 上看到的序列数据和盘口几乎零时差。 库里的品种类(CSymbol)这次也预留了市场深度(DOM)的接口骨架,虽然本小节还没真正接 DOM 数据,但序列结构已经按「可叠加买卖挂单」的方式重排。你在 MT5 里打开专家顾问日志,能看到 2021-03-31 那版示例跑起来后,即时报价序列的更新计数明显比旧版密——旧版每分钟约 8~12 次,新版在活跃品种上能跑到每秒数跳。 想验证很简单:把库里 CSymbol::RefreshTicks() 的调用从 OnTimer 挪到 OnTick,编译挂到 EURUSD 上,开「每笔成交」日志模式看时间戳。外汇和贵金属杠杆高、滑点突发行情多,这种实时性提升在非农秒级波动里可能帮你少接几个不利成交,但也可能让你更频繁地触发策略逻辑,风险敞口反而变大,得自己压参数。

即时报价集合与内存上限的取舍

程序里用到的每个品种都建了独立的即时报价集合,函数库能按品种抓取指定数量的 tick,并统一存进一个可检索的池子里。这个池子支持按对象查找、提取数据,也能直接拉出来做统计整理。 不过原版有个缺口:新品种 tick 进来时,不会自动写进已有数据库。这一节先把“自动入库”补上,让每个新 tick 都扩充集合里的对象数。 对象无限增长会吃内存,所以引入一个常量控制单品种最大存储量,默认 200000 条,大约够覆盖两天的统计研究。若同时跑很多品种且库里已满,函数库会按需要自动淘汰最旧的 tick,始终维持设定上限。 外汇与贵金属 tick 频率高、波动随机,实盘开启多品种采集前应先在 MT5 策略测试器里改这个常量压测内存占用。另外本节开始在品种对象类里预埋市场深度(DOM)广播订阅,具体操控留到下一节。

◍ 给多品种报价与DOM追踪补上底层接口

在 MT5 自建函数库时,OnTick() 只在当前品种来新报价时触发,拿不到别的品种即时 tick。要跟踪跨品种报价,得在库计时器里用之前写的 “New tick” 类对象轮询,这就要求再开一个专管非当前品种报价的定时器。 我们在 \MQL5\Include\DoEasy\Defines.mqh 里加了两个宏:COLLECTION_IND_TS_PAUSE 设为 64 毫秒,是报价序列采集定时器的轮询间隔;COLLECTION_IND_TS_COUNTER_STEP 为 16,是计数器步进值。这两个数字直接决定跨品种 tick 刷新的时效与 CPU 占用,调小可能更跟手但容易拖慢 EA。 品种对象需要记录是否已订阅 DOM 广播。做法是在整数型属性里加一个状态位,并把整数属性总数从 36 改成 37,同时在该品种的排序枚举里挂上这个新属性,后续才能按“是否订阅深度盘口”给品种列表排序。 下面这段代码是往消息索引和文本表里补的 DOM 相关条目,以及定时器宏定义,注意高亮部分就是本次新增的订阅状态消息。

MQL5 / C++
  MSG_SYM_EVENT_SYMBOL_ADD,                      class=class="str">"cmt">// Added symbol to Market Watch window
  MSG_SYM_EVENT_SYMBOL_DEL,                      class=class="str">"cmt">// Symbol removed from Market Watch window
  MSG_SYM_EVENT_SYMBOL_SORT,                     class=class="str">"cmt">// Changed location of symbols in Market Watch window
  MSG_SYM_SYMBOLS_MODE_CURRENT,                  class=class="str">"cmt">// Work with current symbol only
  MSG_SYM_SYMBOLS_MODE_DEFINES,                  class=class="str">"cmt">// Work with predefined symbol list
  MSG_SYM_SYMBOLS_MODE_MARKET_WATCH,             class=class="str">"cmt">// Work with Market Watch window symbols
  MSG_SYM_SYMBOLS_MODE_ALL,                      class=class="str">"cmt">// Work with full list of all available symbols
  MSG_SYM_SYMBOLS_BOOK_ADD,                      class=class="str">"cmt">// Subscribed to Depth of Market 
  MSG_SYM_SYMBOLS_BOOK_DEL,                      class=class="str">"cmt">// Unsubscribed from Depth of Market 
  MSG_SYM_SYMBOLS_MODE_BOOK,                     class=class="str">"cmt">// Subscription to Depth of Market

class=class="str">"cmt">//--- CAccount
  {"В окно \"Обзор рынка\" добавлен символ","Added symbol to \"Market Watch\" window"},
  {"Из окна \"Обзор рынка\" удалён символ","Removed from \"Market Watch\" window"},
  {"Изменено расположение символов в окне \"Обзор рынка\"","Changed arrangement of symbols in \"Market Watch\" window"},
  {"Работа только с текущим символом","Work only with the current symbol"},
  {"Работа с предопределённым списком символов","Work with predefined list of symbols"},
  {"Работа с символами из окна \"Обзор рынка\"","Working with symbols from \"Market Watch\" window"},
  {"Работа с полным списком всех доступных символов","Work with full list of all available symbols"},
  {"Осуществлена подписка на стакан цен ","Subscribed to Depth of Market"},
  {"Осуществлена отписка от стакан цен ","Unsubscribed from Depth of Market"},
  {"Подписка на стакан цен","Subscription to Depth of Market"},

class=class="str">"cmt">//--- CAccount
class=class="str">"cmt">//--- Parameters of the timer of indicator data timeseries collection
class="macro">#define COLLECTION_IND_TS_PAUSE(class="num">64)        class=class="str">"cmt">// Pause of the timer of indicator data timeseries collection in milliseconds
class="macro">#define COLLECTION_IND_TS_COUNTER_STEP(class="num">16)      class=class="str">"cmt">// Increment of indicator data timeseries timer counter

「采集引擎里的定时器与集合清单常量」

在 MT5 用 C++ 风格宏搭一个行情采集框架时,先得把内部计时器和集合容器的 ID 写死成宏,避免运行时字符串查找带来的开销。下面这段定义里,指标数据时间序列的计时器计数器 ID 是 7,而 tick 序列的计时器计数器 ID 是 8,两者分属不同采集通道。 tick 序列采集定时器的暂停间隔被定为 64 毫秒,计数器每次自增步长为 16。也就是说,若按默认参数跑,大约每 64 毫秒触发一次采集、每 16 次计数走一个循环节点,这对外汇或贵金属这种高频 tick 流来说属于可控的轻量轮询,但实盘仍可能因点差跳变产生漏 tick,属正常概率现象。 集合清单用 0x777A 到 0x7783 的一段十六进制 ID 做区分:历史数据、市场、事件、账户、品种、时间序列、缓冲区、指标、指标数据、tick 序列各占一个。文件落地时库目录固定为 "DoEasy\",资源目录为 "DoEasy\Resource\",品种在导航栏的默认背景色宏 CLR_DEFAULT 设为 0xFF000000(不透明黑)。开 MT5 把这套宏贴进头文件,编译零报错即说明你的采集骨架 ID 段没和终端其他库撞车。

MQL5 / C++
class="macro">#define COLLECTION_IND_TS_COUNTER_ID(class="num">7)                 class=class="str">"cmt">// ID of indicator data timeseries timer counter
class=class="str">"cmt">//--- Parameters of the tick series collection timer
class="macro">#define COLLECTION_TICKS_PAUSE(class="num">64)                 class=class="str">"cmt">// Tick series collection timer pause in milliseconds
class="macro">#define COLLECTION_TICKS_COUNTER_STEP(class="num">16)                 class=class="str">"cmt">// Tick series timer counter increment step
class="macro">#define COLLECTION_TICKS_COUNTER_ID(class="num">8)                  class=class="str">"cmt">// Tick series timer counter ID
class=class="str">"cmt">//--- Collection list IDs
class="macro">#define COLLECTION_HISTORY_ID(0x777A)             class=class="str">"cmt">// Historical collection list ID
class="macro">#define COLLECTION_MARKET_ID(0x777B)             class=class="str">"cmt">// Market collection list ID
class="macro">#define COLLECTION_EVENTS_ID(0x777C)             class=class="str">"cmt">// Event collection list ID
class="macro">#define COLLECTION_ACCOUNT_ID(0x777D)             class=class="str">"cmt">// Account collection list ID
class="macro">#define COLLECTION_SYMBOLS_ID(0x777E)             class=class="str">"cmt">// Symbol collection list ID
class="macro">#define COLLECTION_SERIES_ID(0x777F)             class=class="str">"cmt">// Timeseries collection list ID
class="macro">#define COLLECTION_BUFFERS_ID(0x7780)             class=class="str">"cmt">// Indicator buffer collection list ID
class="macro">#define COLLECTION_INDICATORS_ID(0x7781)             class=class="str">"cmt">// Indicator collection list ID
class="macro">#define COLLECTION_INDICATORS_DATA_ID(0x7782)             class=class="str">"cmt">// Indicator data collection list ID
class="macro">#define COLLECTION_TICKSERIES_ID(0x7783)             class=class="str">"cmt">// Tick series collection list ID
class=class="str">"cmt">//--- Data parameters for file operations
class="macro">#define DIRECTORY("DoEasy\")          class=class="str">"cmt">// Library directory for storing object folders
class="macro">#define RESOURCE_DIR("DoEasy\Resource\")  class=class="str">"cmt">// Library directory for storing resource folders
class=class="str">"cmt">//--- Symbol parameters
class="macro">#define CLR_DEFAULT(0xFF000000)         class=class="str">"cmt">// Default symbol background class="type">color in the navigator

MQL5底层宏与符号属性的编译期分叉

在写跨版本 EA 或指标时,MT5 终端构建号直接决定了可遍历品种池的上限。Build 2430 之前 TerminalInfoInteger(TERMINAL_BUILD) 返回的值小于 2430,工作品种总数被压在 1000;2430 及之后放开到 5000,MQL4 环境则恒定 1000,这点不查构建号容易在老终端上漏掉 4000 多个可交易标的。 同步逻辑里写死了几个节奏参数:每次同步尝试间隔 16 毫秒,最多连试 5 次;默认时间序列抓 1000 根 Bar,Tick 序列默认只留 1 天,但单品种 Tick 存储上限锁死在 200000 条。外汇与贵金属波动大,Tick 上限到了之后早期数据会被覆盖,回测历史深度可能因此断档,属高风险注意项。 下面这段预处理与枚举是库的头文件基底,编译期就分好了环境: #ifdef __MQL5__ #define SYMBOLS_COMMON_TOTAL (TerminalInfoInteger(TERMINAL_BUILD)<2430 ? 1000 : 5000) // MQL5工作品种总数随构建号变化 #else #define SYMBOLS_COMMON_TOTAL (1000) // MQL4工作品种总数固定 #endif #define PENDING_REQUEST_ID_TYPE_ERR (1) // 基于服务器返回码建的挂起请求类型 #define PENDING_REQUEST_ID_TYPE_REQ (2) // 主动建的挂起请求类型 #define SERIES_DEFAULT_BARS_COUNT (1000) // 默认时序数据量 #define PAUSE_FOR_SYNC_ATTEMPTS (16) // 同步尝试间暂停毫秒 #define ATTEMPTS_FOR_SYNC (5) // 同步尝试次数 #define TICKSERIES_DEFAULT_DAYS_COUNT (1) // 默认Tick天数 #define TICKSERIES_MAX_DATA_TOTAL (200000) // 单品种Tick存储上限 enum ENUM_SYMBOL_PROP_INTEGER { SYMBOL_PROP_STATUS = 0, // 符号状态 SYMBOL_PROP_INDEX_MW, // 市场报价窗内索引 SYMBOL_PROP_CUSTOM, // 自定义品种标记 SYMBOL_PROP_CHART_MODE, // 生成Bar用的价格类型(Bid/Last) SYMBOL_PROP_EXIST // 该名符号是否存在的标记 }; 开 MT5 按 F4 把这段贴进一个新 include 头文件,编译后打印 SYMBOLS_COMMON_TOTAL 的值,就能确认你当前终端落在哪个品种上限区间。

MQL5 / C++
class="macro">#ifdef __MQL5__
  class="macro">#define SYMBOLS_COMMON_TOTAL(TerminalInfoInteger(TERMINAL_BUILD)<class="num">2430 ? class="num">1000 : class="num">5000) class=class="str">"cmt">// Total number of MQL5 working symbols
class="macro">#else
  class="macro">#define SYMBOLS_COMMON_TOTAL(class="num">1000) class=class="str">"cmt">// Total number of MQL4 working symbols
class="macro">#endif
class=class="str">"cmt">//--- Pending request type IDs
class="macro">#define PENDING_REQUEST_ID_TYPE_ERR(class="num">1) class=class="str">"cmt">// Type of a pending request created based on the server class="kw">return code
class="macro">#define PENDING_REQUEST_ID_TYPE_REQ(class="num">2) class=class="str">"cmt">// Type of a pending request created by request
class=class="str">"cmt">//--- Timeseries parameters
class="macro">#define SERIES_DEFAULT_BARS_COUNT(class="num">1000) class=class="str">"cmt">// Required class="kw">default amount of timeseries data
class="macro">#define PAUSE_FOR_SYNC_ATTEMPTS(class="num">16) class=class="str">"cmt">// Amount of pause milliseconds between synchronization attempts
class="macro">#define ATTEMPTS_FOR_SYNC(class="num">5) class=class="str">"cmt">// Number of attempts to receive synchronization with the server
class=class="str">"cmt">//--- Tick series parameters
class="macro">#define TICKSERIES_DEFAULT_DAYS_COUNT(class="num">1) class=class="str">"cmt">// Required number of days for tick data in class="kw">default series
class="macro">#define TICKSERIES_MAX_DATA_TOTAL(class="num">200000) class=class="str">"cmt">// Maximum number of stored tick data of a single symbol
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Symbol integer properties                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_SYMBOL_PROP_INTEGER
  {
   SYMBOL_PROP_STATUS = class="num">0, class=class="str">"cmt">// Symbol status
   SYMBOL_PROP_INDEX_MW, class=class="str">"cmt">// Symbol index in the Market Watch window
   SYMBOL_PROP_CUSTOM, class=class="str">"cmt">// Custom symbol flag
   SYMBOL_PROP_CHART_MODE, class=class="str">"cmt">// The price type used for generating bars – Bid or Last(from the ENUM_SYMBOL_CHART_MODE enumeration)
   SYMBOL_PROP_EXIST class=class="str">"cmt">// Flag indicating that the symbol under this name exists
  };

◍ 市场观察与交易属性枚举的收尾字段

上面这些 SYMBOL_PROP_* 枚举值,是 MT5 里 SymbolInfoInteger 能直接读到的品种属性尾部清单。从 SELECT、VISIBLE 到 EXPIRATION_TIME,覆盖了对市场观察窗口状态、当前会话挂单统计、点差类型、合约计算模式与交易起止日的访问入口。 其中 SYMBOL_PROP_BOOKDEPTH_STATE 标黄那行值得单独看:它返回的是当前是否订阅了指定品种的 DOM(市场深度)数据,而非深度本身。做贵金属 scalping 时若发现 Book 事件不触发,先查这个标志位比怀疑代码逻辑更效率。外汇与贵金属杠杆交易风险偏高,订阅状态异常可能让你漏掉流动性突变信号。 把这些常量原样贴进脚本,配合 SymbolInfoInteger(_Symbol, SYMBOL_PROP_SPREAD_FLOAT) 之类调用,能在开盘前扫一遍品种配置。下面片段就是这一组枚举的原文定义,直接可用于头文件或调试输出。

MQL5 / C++
  SYMBOL_PROP_SELECT,                                                                     class=class="str">"cmt">// The indication that the symbol is selected in Market Watch
  SYMBOL_PROP_VISIBLE,                                                                     class=class="str">"cmt">// The indication that the symbol is displayed in Market Watch
  SYMBOL_PROP_SESSION_DEALS,                                                               class=class="str">"cmt">// The number of deals in the current session 
  SYMBOL_PROP_SESSION_BUY_ORDERS,                                                          class=class="str">"cmt">// The total number of Buy orders at the moment
  SYMBOL_PROP_SESSION_SELL_ORDERS,                                                         class=class="str">"cmt">// The total number of Sell orders at the moment
  SYMBOL_PROP_VOLUME,                                                                      class=class="str">"cmt">// Last deal volume
  SYMBOL_PROP_VOLUMEHIGH,                                                                  class=class="str">"cmt">// Maximum volume within a day
  SYMBOL_PROP_VOLUMELOW,                                                                   class=class="str">"cmt">// Minimum volume within a day
  SYMBOL_PROP_TIME,                                                                        class=class="str">"cmt">// Latest quote time
  SYMBOL_PROP_DIGITS,                                                                      class=class="str">"cmt">// Number of decimal places
  SYMBOL_PROP_DIGITS_LOTS,                                                                 class=class="str">"cmt">// Number of decimal places for a lot
  SYMBOL_PROP_SPREAD,                                                                      class=class="str">"cmt">// Spread in points
  SYMBOL_PROP_SPREAD_FLOAT,                                                                class=class="str">"cmt">// Floating spread flag
  SYMBOL_PROP_TICKS_BOOKDEPTH,                                                             class=class="str">"cmt">// Maximum number of orders displayed in the Depth of Market
  SYMBOL_PROP_BOOKDEPTH_STATE,                                                             class=class="str">"cmt">// Flag of subscription to DOM
  SYMBOL_PROP_TRADE_CALC_MODE,                                                             class=class="str">"cmt">// Contract price calculation method(from the ENUM_SYMBOL_CALC_MODE enumeration)
  SYMBOL_PROP_TRADE_MODE,                                                                  class=class="str">"cmt">// Order execution type(from the ENUM_SYMBOL_TRADE_MODE enumeration)
  SYMBOL_PROP_START_TIME,                                                                  class=class="str">"cmt">// Symbol trading start date(usually used for futures)
  SYMBOL_PROP_EXPIRATION_TIME                                                             class=class="str">"cmt">// Symbol trading end date(usually used for futures)
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时报价序列的滚动状态与内存占用,把重复劳动交给小布,你专注决策。

常见问题

当前品种新 tick 到达才触发 OnTick,其他品种需靠库内独立定时器轮询“New tick”对象来捕获变动并写入各自序列。
默认上限 200000 根,约覆盖两天统计量;在 Defines.mqh 的常数里可按品种单独调整最大存储数。
达到上限后库自动按量剔除最旧对象,腾出内存;若研究窗口长,应提前调大常数而非依赖默认。
可以,小布盯盘对应品种页已内置 AIGC 诊断,实时呈现序列长度与内存占用,省去自己打日志。
它在品种整数属性里标记是否已订阅市场深度广播,为下一篇真正操控 DOM 做前置标识。