DoEasy 函数库中的时间序列(第三十九部分):基于函数库的指标 - 准备数据和时间序列事件·进阶篇
(2/3)·指标线程被一个慢脚本拖垮?这一篇讲清时间序列在指标里的加载与事件分派
「交易类里那道给指标让路的闸门」
在封装交易动作的 C++ 风格类里,ClosePositionBy、ModifyPosition、SetOrder 这几个方法开头都先判了一次 m_program。若当前运行环境是指标(PROGRAM_INDICATOR)或服务(PROGRAM_SERVICE),方法直接 return true,不执行任何真实下单或改单。 这个设计意味着:你把同一套 CTradeObj 逻辑挂在自定义指标里跑,它不会真去碰账户,只是静默返回成功。回测或调试时若发现指标里调用交易函数没反应,先查是不是被这道闸门拦了,而不是去怀疑经纪商拒绝。 真实交易只允许在 EA(PROGRAM_EXPERT)或脚本环境下越过该判断,后续 ResetLastError() 才真正清错误码准备发单。外汇与贵金属杠杆高,这类封装若误用在信号指标里,可能让你以为挂单成功实际毫无动作,需自行在日志里补一行 Print 验证。
class="type">bool CTradeObj::ClosePositionBy(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">ulong ticket_by) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) class="kw">return true; ::ResetLastError(); class="type">bool CTradeObj::ModifyPosition(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">double sl=WRONG_VALUE,class="kw">const class="type">class="kw">double tp=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) class="kw">return true; ::ResetLastError(); class="type">bool CTradeObj::SetOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double volume, class="kw">const class="type">class="kw">double price, class="kw">const class="type">class="kw">double sl=class="num">0, class="kw">const class="type">class="kw">double tp=class="num">0, class="kw">const class="type">class="kw">double price_stoplimit=class="num">0, class="kw">const class="type">class="kw">ulong magic=ULONG_MAX, class="kw">const class="type">class="kw">string comment=NULL, class="kw">const class="type">class="kw">datetime expiration=class="num">0, class="kw">const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE, class="kw">const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE)
挂单操作的类方法骨架与指标环境拦截
CTradeObj 里删单与改单的两个方法,开头都先判断运行环境:当 m_program 是 PROGRAM_INDICATOR 或 PROGRAM_SERVICE 时直接 return true,意味着指标或系统服务里不真发删改指令,只是走个过场。实盘 EA 之外调用这套封装容易误以为单子已撤,其实什么都没发生。 DeleteOrder 只收一个 ticket 参数,进入后先 ::ResetLastError() 清掉之前的错误码,后续若要接 OrderDelete 就得自己补校验。ModifyOrder 则把 price、sl、tp、price_stoplimit、expiration、type_time、type_filling 全设了 WRONG_VALUE 默认值,调用方不传就代表不改该项,这种写法在调参时少写几个实参能省不少事。 CBar 的构造里用 MqlRates rates_array[1] 只开一个元素,靠 SetSymbolPeriod(symbol,timeframe,index) 把指定 K 线塞进去,同样以 ::ResetLastError() 开局。若按 index 取不到数据,注释里写的是填零结构兜底,开 MT5 挂个指标跑这段就能看到空 bar 不崩但数值全 0。
class="type">bool CTradeObj::DeleteOrder(class="kw">const class="type">class="kw">ulong ticket) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) class="kw">return true; ::ResetLastError(); } class="type">bool CTradeObj::ModifyOrder(class="kw">const class="type">class="kw">ulong ticket, class="kw">const class="type">class="kw">double price=WRONG_VALUE, class="kw">const class="type">class="kw">double sl=WRONG_VALUE, class="kw">const class="type">class="kw">double tp=WRONG_VALUE, class="kw">const class="type">class="kw">double price_stoplimit=WRONG_VALUE, class="kw">const class="type">class="kw">datetime expiration=WRONG_VALUE, class="kw">const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE, class="kw">const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) class="kw">return true; ::ResetLastError(); } CBar::CBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { this.m_type=COLLECTION_SERIES_ID; class="type">MqlRates rates_array[class="num">1]; this.SetSymbolPeriod(symbol,timeframe,index); ::ResetLastError(); }
◍ K线对象构造时的报错兜底逻辑
在封装 CBar 类时,从 CopyRates 取单根 K 线数据不是永远成功的。若返回条数小于 1,说明这次请求没拿到有效行情,此时必须用 GetLastError 抓错误码,否则后续用 rates_array[0] 会读到脏数据。 下面这段处理把失败情况兜死了:先取 err_code 并打印包含错误描述与返回码的信息,再构造一个全零的 MqlRates 赋给 rates_array[0],保证对象不至于崩在空引用上。 [CODE] if(::CopyRates(symbol,timeframe,index,1,rates_array)<1) { int err_code=::GetLastError(); ::Print ( DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); MqlRates err={0}; rates_array[0]=err; } ::ResetLastError(); [/CODE] 另一处坑在 TimeToStruct:即便 rates_array[0].time 拿到了,转成时间结构也可能失败(比如时间越界)。失败分支里同样抓错误码、打印,但不覆盖原 rates,只是跳过结构赋值。 [CODE] if(!::TimeToStruct(rates_array[0].time,this.m_dt_struct)) { int err_code=::GetLastError(); ::Print ( DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); } [/CODE] 第二个构造函数走的是直接传 MqlRates 引用,不再调 CopyRates,但仍要对 rates.time 做 TimeToStruct 校验。失败时打印 "(2) " 前缀区分调用源,并用全零 MqlRates 走 SetProperties 保底。外汇与贵金属市场跳空频繁,这类兜底能降低 EA 在异常行情下静默出错的概率。
if(::CopyRates(symbol,timeframe,index,class="num">1,rates_array)<class="num">1) { class="type">int err_code=::GetLastError(); ::Print( DFUN,"(class="num">1) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); class="type">MqlRates err={class="num">0}; rates_array[class="num">0]=err; } ::ResetLastError(); if(!::TimeToStruct(rates_array[class="num">0].time,this.m_dt_struct)) { class="type">int err_code=::GetLastError(); ::Print( DFUN,"(class="num">1) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); } this.SetProperties(rates_array[class="num">0]); } CBar::CBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index,class="kw">const class="type">MqlRates &rates) { this.m_type=COLLECTION_SERIES_ID; this.SetSymbolPeriod(symbol,timeframe,index); ::ResetLastError(); if(!::TimeToStruct(rates.time,this.m_dt_struct)) { class="type">int err_code=::GetLastError(); ::Print( DFUN,"(class="num">2) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); class="type">MqlRates err={class="num">0}; this.SetProperties(err);
「把行情数据装进结构体再算」
在自定义指标里,与其让 OnCalculate 的参数散落各处,不如先定义一个 SDataCalculate 结构体把上下文一次性收口。下面这段代码声明了 rates_total、prev_calculated、begin、price 和 rates 五个字段,分别对应输入序列长度、上次已处理柱数、有效数据起点、当前计算价和 MqlRates 行情结构。
把数据塞进结构体后,主计算函数只认这一个 rates_data 实例,逻辑清爽很多。OnCalculate 的标准签名也必须守住:rates_total 是 price[] 数组大小,prev_calculated 是上一次调用已处理的柱数,begin 是 price[] 中有效数据的起始下标,price[] 是待计算的值数组。
在 MT5 里新建一个指标,把结构体原样贴进去,再在 OnCalculate 开头写 rates_data.rates_total=rates_total; 这类赋值,就能直接观察每次刷新时 prev_calculated 从 0 涨到 rates_total 的过程,验证增量计算是否生效。外汇与贵金属杠杆高,验证时先用模拟盘跑,避免实盘误触。
class="kw">struct SDataCalculate { class="type">int rates_total; class=class="str">"cmt">// size of class="kw">input time series class="type">int prev_calculated; class=class="str">"cmt">// number of handled bars at the previous call class="type">int begin; class=class="str">"cmt">// where significant data start class="type">class="kw">double price; class=class="str">"cmt">// current array value for calculation class="type">MqlRates rates; class=class="str">"cmt">// Price structure } rates_data; class="type">int OnCalculate( class="kw">const class="type">int rates_total, class=class="str">"cmt">// price[] array size class="kw">const class="type">int prev_calculated, class=class="str">"cmt">// number of handled bars at the previous call class="kw">const class="type">int begin, class=class="str">"cmt">// index number in the price[] array meaningful data starts from class="kw">const class="type">class="kw">double& price[] class=class="str">"cmt">// array of values for calculation );
时间序列容器的参数引用与切换逻辑
在 MT5 自定义类里处理 K 线序列,最底层是把八类数组以 const 引用方式传进来处理函数:时间、开高低收、 tick 成交量、真实成交量、点差。这样写不会复制数据,直接挂在对应图表缓冲上,回测和实盘里内存占用都更稳。 切换品种或周期时,SetSymbol 与 SetTimeframe 都带一个默认 false 的 set_server_date 参数。只有显式传 true,才会顺带调用 SetServerDate 去对齐服务器时间基准;不传就只改本地成员,避免每次换符号都触发一次时间同步。 注意 SetSymbol 里对空串或 NULL 的兜底:自动回退到当前图表 Symbol()。外汇与贵金属点差跳变频繁,若你的逻辑依赖跨品种比对,建议显式传 symbol 而非依赖默认,否则可能抓到非预期品种。 下面这段是类方法骨架,开 MT5 新建 EA 把 CSeries 补完即可验证引用传递效果。
class="kw">const class="type">class="kw">datetime& time, class=class="str">"cmt">// Time array class="kw">const class="type">class="kw">double& open, class=class="str">"cmt">// Open array class="kw">const class="type">class="kw">double& high, class=class="str">"cmt">// High array class="kw">const class="type">class="kw">double& low, class=class="str">"cmt">// Low array class="kw">const class="type">class="kw">double& close, class=class="str">"cmt">// Close array class="kw">const class="type">long& tick_volume, class=class="str">"cmt">// Tick Volume array class="kw">const class="type">long& volume, class=class="str">"cmt">// Real Volume array class="kw">const class="type">int& spread class=class="str">"cmt">// Spread array ); class=class="str">"cmt">//--- Set(class="num">1) symbol, (class="num">2) timeframe, (class="num">3) symbol and timeframe, (class="num">4) amount of applied timeseries data class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">bool set_server_date=class="kw">false); class="type">void SetTimeframe(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">bool set_server_date=class="kw">false); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set a symbol | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSeries::SetSymbol(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">bool set_server_date=class="kw">false) { if(this.m_symbol==symbol) class="kw">return; this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_new_bar_obj.SetSymbol(this.m_symbol); if(set_server_date) this.SetServerDate(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set a timeframe | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSeries::SetTimeframe(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">bool set_server_date=class="kw">false) { if(this.m_timeframe==timeframe) class="kw">return; this.m_timeframe=(timeframe==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)::Period() : timeframe); this.m_new_bar_obj.SetPeriod(this.m_timeframe); this.m_period_description=TimeframeDescription(this.m_timeframe); if(set_server_date)
◍ 手动判定新 K 线后的序列刷新路径
CSeries::Refresh 的核心逻辑是先判断该序列是否被启用,未启用的直接 return,避免无谓的 CopyRates 调用。启用后会按 BAR_INDEX 对已有 K 线链表排序,再调用 IsNewBarManual 比对 data_calculate.rates.time 与已记录时间,决定是否生成新 CBar 对象并插入链表。 插入成功后会执行 SetServerDate 更新服务器时间与最后一根 K 线开盘时间,随后做容量裁剪:当 m_list_series.Total() 大于 m_required 时,删除索引 0 的最早一根。这意味着你设定的 m_required 直接决定内存里保留多少根历史 Bar,调小它能降内存但会丢掉早期形态。 SetSymbolPeriod 里有个细节值得注意:当 symbol 与 timeframe 均未变化时直接 return,不重复 SetSymbol 和 SetTimeframe。这在多品种多周期监听时可以减少对象内部状态重置,但也意味着切换品种必须保证传入参数确实不同,否则序列不会更新。外汇与贵金属波动剧烈,这类手动刷新逻辑在高波动时段可能漏掉瞬时跳空新 Bar,建议在 MT5 用 EURUSD 的 M1 实测一次新 Bar 插入耗时。
class="type">void CSeries::SetSymbolPeriod(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe) { if(this.m_symbol==symbol && this.m_timeframe==timeframe) class="kw">return; this.SetSymbol(symbol); this.SetTimeframe(timeframe,true); } class="type">void CSeries::Refresh(SDataCalculate &data_calculate) { class=class="str">"cmt">//--- If the timeseries is not used, exit if(!this.m_available) class="kw">return; class="type">MqlRates rates[class="num">1]; class=class="str">"cmt">//--- Set the flag of sorting the list of bars by index this.m_list_series.Sort(SORT_BY_BAR_INDEX); class=class="str">"cmt">//--- If a new bar is present on a symbol and period, if(this.IsNewBarManual(data_calculate.rates.time)) { CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,class="num">0); if(new_bar==NULL) class="kw">return; if(!this.m_list_series.InsertSort(new_bar)) { class="kw">delete new_bar; class="kw">return; } this.SetServerDate(); if(this.m_list_series.Total()>(class="type">int)this.m_required) this.m_list_series.Delete(class="num">0); } }