DoEasy 函数库中的时间序列(第三十九部分):基于函数库的指标 - 准备数据和时间序列事件(基础篇)
📘

DoEasy 函数库中的时间序列(第三十九部分):基于函数库的指标 - 准备数据和时间序列事件(基础篇)

第 1/3 篇

◍ 给指标接上时间序列事件

DoEasy 函数库做到第三十九部分,核心是把「指标」也纳入时间序列的统一管控。之前库里已经能处理价格序列和各类事件,这一节要让指标数据像普通时间序列一样被监听、被触发。 改进点集中在操控指标的类:给它补上事件接口,使得指标缓冲区刷新、新柱形成这类动作能直接丢进时间序列事件队列。这样写 EA 时不用再手动轮询 IndicatorBuffer,事件驱动即可。 原文给出的示例发布于 2020-07-24,在 MT5 官方社区获得 2324 次查看、21 条讨论,说明这套封装对实盘开发者有真实需求。外汇与贵金属市场波动剧烈、杠杆高风险大,事件驱动的漏触发或重触发都可能在极端行情放大滑点。 下一步是在具体指标里跑这套事件,验证时间序列抽象层是否扛得住高频 tick 下的缓冲更新。

指标线程与历史数据请求的坑

EA 和脚本跑在各自逻辑里,指标却是另一套架构:每个指标都在其启动品种的单线程中执行。同一品种多个图表上的不同指标,全挤在这个品种线程里。只要其中一个指标架构有缺陷变慢,同线程的其他指标会被冻结,干等那个“慢指标”算完。 为了避免等历史数据卡住,终端提供了顺序返回机制——激活加载历史数据的函数会立刻返回结果,不阻塞。但 Copy 函数请求跨品种时间序列时,指标和 EA 行为完全不同:指标侧若请求的时间序列还没构建或从服务器下载中,函数立即返回 -1;EA/脚本侧若本地缺数据,则触发从服务器下载,函数在超时前返回已就绪的部分,下载在后台继续,下次请求拿更多。 这带来一个实打实的限制:指标不能等数据。第一次请求没本地历史就触发下载,但超时前不阻塞,当前 tick 的计算代码必须退出,等新报价进 OnCalculate() 时再判断 rates_total 和 prev_calculated,决定手头数据够不够跑算法。 指标还绝不能自己下载“本品种本周期”的数据,否则会和终端子系统冲突。这部分由终端在 OnCalculate() 里通过那两个变量喂给你。我们接下来要调时间序列类,让指标初始化时加载到正确数据,并把实时更新事件发回控制程序图表。

「重做时间序列类以避免测试器指针陷阱」

把指标里的交易函数调用彻底堵死,是这次改 BaseObj 和 TradeObj 的底色。CBaseObj 构造函数里 m_available 初始化由 false 改为 true,对象一出生就是“已用”状态;CBaseObjExt 里原来的 SetEvent() 改名为 SetEventFlag(),只为消除“这是发事件还是标事件”的歧义。跨平台交易类 TradeObj.mqh 每个方法开头都插了程序类型检查,指标或服务环境下直接 return true,不碰任何交易接口,库内错误就此断根。 CSeries 类现在给设置品种/周期的方法挂了一个“是否顺带设服务器日期”的开关,默认关。先调设置品种(关标记)再调设置周期(开标记),只在周期方法里触发一次日期重置,避免重复清洗。Refresh() 也不再收一堆数组,改成吃 Defines.mqh 里新建的 SOnCalcData 结构——这个结构把 rates_total、prev_calculated、begin、price 或 MqlRates 数组按 OnCalculate 两种形态打包,一次传透。 CTimeSeries 之前图省事,把终端所有时间帧的指针都存进列表,结果可视测试器会把没用到的周期也建图,运行时切周期还会跟踪已死对象。现在改成只留真正用到的时间序列指针:AddSeries() 按时间帧建临时对象、排序列表、搜重,搜不到才插入并置“已用”标志。SetRequiredAllUsedData() 和 SyncAllData() 的循环上限也从“全部时间帧数”换成“列表实际对象数”。 事件链路顺着 CBaseObjExt 走:每次 Refresh()/RefreshAll() 里给列表对象打事件标志,集合类 CTimeSeriesCollection 再把这些标志捞出来发往控制图表。GetTimeseries() 用第三十七篇的 IndexTimeSeries() 拿索引,GetSeries() 用 IndexTimeframe() 按周期定位;重建逻辑里特意跳过初始化时同品种同周期冲突,进 OnCalculate 拿到 rates_total 后再 RecreateSeries() 填空序列。 一个可验证点:旧逻辑下列表长度恒等于 ENUM_TIMEFRAMES 常量数(主终端通常 21),新逻辑下列表长度 = 程序实际创建的周期数,EA 初始化后可在调试里打印 m_list.SeriesTotal() 比对。外汇/贵金属品种切换周期时历史不足属常态,按 MetaQuotes 建议每次即时报价重检空序列即可,高风险环境别假设数据必齐。

◍ 给 CEngine 主对象接上时间序列集合的操控能力

把时间序列类的改进收口,下一步是动 CEngine 主对象(\MQL5\Include\DoEasy\Engine.mqh),让程序真正能操控一整套时间序列集合,而不只是零散调用。 类的私有段先声明一个暂停对象,公开段加上返回集合事件标志的接口,它直接转发时间序列集合对象的 IsEvent() 结果。OnCalculate() 送进来的数组,现在要改投到刷新当前序列的方法里,同时在 Timer 和 Tick 处理中补上数组结构传递与 Calculate 事件应答声明。 公开段再补一个返回事件列表的接口,内部调用 GetListEvents() 拿到指针;原来四个建序列的方法砍掉三个,换成按所用品种批量建序列、按指定重建、以及与服务器同步请求这三个。刷新方法只留两个:更新指定序列和更新全集合,入参由单纯 OnCalculate() 数组升级为含变量的结构体加数组。 另加四个取指针的接口——按品种、按序列对象、取空序列、取部分填充序列,均透传集合的同名方法。TradingOnInit() 改名为 CollectionOnInit(),语义上更贴合所有集合都要初始化的事实。 计时器逻辑也重排了:原先每个应答程序都重复判断是否在测试器,现在统一在入口分流「非测试器 / 测试器」两大块,内部再处理各集合,应答程序明显变短,也少了一堆无效判断。 空序列同步是历史加载的基石:每次即时报价取第一个未填充序列,尝试与服务器同步,失败就等下一 tick;成功则用不超过请求数量的历史柱重建。这个过程循环到没有空序列为止。NewTick 和 Calculate 里都会调重建空序列的方法。 暂停对象沿用第三十篇讲过的 CPause,额外加了一个 Pause() 免初始化重载,毫秒数直接当入参传。它在指标里顶替 Sleep() 卡主线程,但只在必要处用,否则会拖慢整个指标。外汇与贵金属波动剧烈,这类主线程延迟在高波动时段可能放大滑点风险。

MQL5 / C++
  MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY,        class=class="str">"cmt">// Failed to prepare array of used symbols. Error 
  MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY,               class=class="str">"cmt">// Failed to get array of used symbols.
  MSG_LIB_SYS_ERROR_EMPTY_PERIODS_STRING,             class=class="str">"cmt">// Error. The class="type">class="kw">string of predefined periods is empty and is to be used
class=class="str">"cmt">//--- CBar
  MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA,               class=class="str">"cmt">// Failed to receive bar data
  MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE,            class=class="str">"cmt">// Failed to write time to time structure
  MSG_LIB_TEXT_BAR_FAILED_GET_SERIES_DATA,            class=class="str">"cmt">// Failed to receive timeseries data
  MSG_LIB_TEXT_TS_TEXT_SYMBOL_TERMINAL_FIRSTDATE,     class=class="str">"cmt">// The very first date in history by a symbol in the client terminal
  MSG_LIB_TEXT_TS_TEXT_CREATED_OK,                    class=class="str">"cmt">// successfully created
  MSG_LIB_TEXT_TS_TEXT_NOT_CREATED,                   class=class="str">"cmt">// not created
  MSG_LIB_TEXT_TS_TEXT_IS_SYNC,                       class=class="str">"cmt">// synchronized
  MSG_LIB_TEXT_TS_TEXT_ATTEMPT,                       class=class="str">"cmt">// Attempt:
  MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC,                 class=class="str">"cmt">// Waiting for data synchronization ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
  {"Не удалось подготовить массив используемых символов. Ошибка ","Failed to create an array of used symbols. Error "},
  {"Не удалось получить массив используемых символов","Failed to get array of used symbols"},
  {"Ошибка. Строка предопределённых периодов пустая, будет использоваться ","Error. String of predefined periods is empty, the Period will be used: "},
  {"Не удалось получить данные бара","Failed to get bar data"},
  {"Не удалось записать время в структуру времени","Failed to write time to class="type">class="kw">datetime structure"},
  {"Не удалось получить данные таймсерии","Failed to get timeseries data"},

基类构造里的多语言日志与可用性开关

这段 CBaseObj 构造函数把多语言提示字符串塞进对象,方便后续在客户端终端里输出「Very first date in history of symbol in client terminal」这类状态。俄语与英文成对出现,说明日志系统面向多语种终端,调试时切语言不用改代码。 构造列表里 m_available(true) 与 m_first_start(true) 是高亮字段:对象默认可用且标记首次启动,意味着策略加载后第一次跑逻辑会走初始化分支,之后靠 m_first_start 翻转跳过。外汇与贵金属品种加载时若历史数据缺失,这个首次标记可能影响同步等待的触发频率,属于高风险的终端环境差异点。 SetEventFlag 只有一行:把外部传入的 bool 写进 m_is_event。它不返回状态,调用方需自己保证事件标志语义一致,否则持仓事件可能被静默覆盖。 下面拆一下构造函数核心行: CBaseObj() : m_program((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)) // 取当前程序类型(EA/指标/脚本)存入 m_program , m_global_error(ERR_SUCCESS) // 全局错误码初始清零 , m_log_level(LOG_LEVEL_ERROR_MSG) // 日志仅记错误级 , m_chart_id_main(::ChartID()) // 主图 ID 绑定当前图表 , m_chart_id(::ChartID()) // 工作图 ID 同主图 , m_folder_name(DIRECTORY) // 文件目录常量 , m_sound_name("") // 无声效文件 , m_name(__FUNCTION__) // 对象名取函数名 , m_type(0) // 子类型默认 0 , m_use_sound(false) // 关闭声效 , m_available(true) // 对象默认可用 , m_first_start(true) {} // 标记首次启动

MQL5 / C++
CBaseObj() : m_program((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)),
              m_global_error(ERR_SUCCESS),
              m_log_level(LOG_LEVEL_ERROR_MSG),
              m_chart_id_main(::ChartID()),
              m_chart_id(::ChartID()),
              m_folder_name(DIRECTORY),
              m_sound_name(""),
              m_name(__FUNCTION__),
              m_type(class="num">0),
              m_use_sound(class="kw">false),
              m_available(true),
              m_first_start(true) {}

class="type">void SetEventFlag(class="kw">const class="type">bool flag) { this.m_is_event=flag; }

「指标环境里交易函数直接放行」

在封装交易对象的代码里,ClosePosition 与 ClosePositionPartially 等方法的开头都先做了一道程序类型判断。若当前运行环境是指标(PROGRAM_INDICATOR)或服务(PROGRAM_SERVICE),函数直接 return true,不执行任何下单或平仓动作。 这段逻辑的实际含义很直接:指标和服务程序本身没有交易权限,若在它们内部调用交易方法,底层根本发不出订单。代码里用 this.m_program 比对类型,命中后提前返回,避免了无意义的报错堆积。 注意 ResetLastError() 的调用位置——它在类型判断之后、真正交易逻辑之前执行,说明只有到了具备交易能力的 EA 环境,才需要清错误码再干活。外汇与贵金属交易自带高杠杆风险,这类封装能减少误调用,但无法替代仓位与风控纪律。 想在 MT5 里验证很简单:把 CTradeObj 丢进一个自定义指标,调用 ClosePosition,断点会卡在 return true 那行,终端交易面板的持仓纹丝不动。

MQL5 / C++
class="type">bool CTradeObj::ClosePosition(class="kw">const class="type">class="kw">ulong ticket,
                                class="kw">const class="type">class="kw">string comment=NULL,
                                class="kw">const class="type">class="kw">ulong deviation=ULONG_MAX)
  {
  if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
    class="kw">return true;
  ::ResetLastError();
  }

class="type">bool CTradeObj::ClosePositionPartially(class="kw">const class="type">class="kw">ulong ticket,
                                        class="kw">const class="type">class="kw">double volume,
                                        class="kw">const class="type">class="kw">string comment=NULL,
                                        class="kw">const class="type">class="kw">ulong deviation=ULONG_MAX)
  {
  if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
    class="kw">return true;
  ::ResetLastError();
  }

常见问题

多半是时间序列类在测试器里用了真实账户才有的指针,重做基类把测试环境指针陷阱隔离掉就能解决。
指标环境默认不放行交易函数,需要在基类构造时显式打开可用性开关,否则只能读数据不能操作。
可以,小布能扫描你的指标结构,标出事件绑定缺失和测试器指针风险点,并给出修改建议。
在构造里用轻量开关控制日志输出,默认关掉只留错误级,需要时再开调试语种。
指标线程请求历史必须在初始化阶段完成,运行中跨线程拉数据会卡死,操控接口要限制为只读缓存。