DoEasy 函数库中的时间序列(第六十部分):品种即时报价数据的序列列表·综合运用
(3/3)· 用列表统一管理各品种当天及历史 Tick,搜索比对不再靠手算循环
「TickSeries 类的接口骨架与比较逻辑」
在 MT5 自建 tick 序列类时,先得把对象可比性和基础访问接口定清楚。下面这段声明里,Compare 方法用 Symbol() 做三态比较:相等返回 0,当前对象交易品种名更大返回 1,否则返回 -1,这让 tick 序列能直接丢进排序容器。 类里还暴露了 Header()、Print()、PrintShort() 三个 journal 输出方法,以及带默认参数的两个构造函数:无参版本留空,含 symbol 和 required(默认 0)的版本用于实际绑定品种与预取数据量。 SetSymbol 与 SetRequiredUsedBars 负责运行时改绑定;而 Symbol()、AvailableUsedData()、RequiredUsedDays()、IsNewTick() 全是只读 inline 取值器,其中 IsNewTick 转调内部 m_new_tick_obj.IsNewTick(),新 tick 判定延迟极低。 后续按索引取价的接口从 Bid(const uint index) 起头,配合 Ask / Last / 高精度 volume / spread 等 9 类取值,构成逐 tick 回放的基础。开 MT5 把这段声明塞进头文件,编译过即说明你的 tick 容器接口层没写错。
class="kw">const CTickSeries *compared_obj=node; class="kw">return(this.Symbol()==compared_obj.Symbol() ? class="num">0 : this.Symbol()>compared_obj.Symbol() ? class="num">1 : -class="num">1); class=class="str">"cmt">//--- Return the tick series name class="type">class="kw">string Header(class="type">void); class=class="str">"cmt">//--- Display(class="num">1) the tick series description and(class="num">2) the tick series class="type">class="kw">short description in the journal class="type">void Print(class="type">void); class="type">void PrintShort(class="type">void); class=class="str">"cmt">//--- Constructors CTickSeries(class="type">void){;} CTickSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Methods of working with objects and accessing their properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Set(class="num">1) a symbol and(class="num">2) a number of used tick series data class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol); class="type">void SetRequiredUsedBars(class="kw">const class="type">uint required=class="num">0); class=class="str">"cmt">//--- Return(class="num">1) symbol, (class="num">2) number of used, (class="num">3) requested tick data and(class="num">4) new tick flag class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; } class="type">class="kw">ulong AvailableUsedData(class="type">void) class="kw">const { class="kw">return this.m_amount; } class="type">class="kw">ulong RequiredUsedDays(class="type">void) class="kw">const { class="kw">return this.m_required; } class="type">bool IsNewTick(class="type">void) { class="kw">return this.m_new_tick_obj.IsNewTick(); } class=class="str">"cmt">//--- Return(class="num">1) Bid, (class="num">2) Ask, (class="num">3) Last, (class="num">4) volume with increased accuracy, class=class="str">"cmt">//--- (class="num">5) spread, (class="num">6) volume, (class="num">7) tick flags, (class="num">8) time, (class="num">9) time in milliseconds by index in the list class="type">class="kw">double Bid(class="kw">const class="type">uint index);
用索引和毫秒时间抓逐笔报价
CTickSeries 这套接口把每一笔 tick 的字段拆得很细:Bid、Ask、Last、VolumeReal、Spread、Volume、Flags,外加 Time 与 TimeMSC。前一组按 uint 索引取数,适合在已知 tick 序号时直接回放,比如 index=0 拿到最新一笔,index 越大越旧。 后两组重载按时间取数,区别在精度。time_msc 用 ulong 毫秒戳,能定位到同一秒内的不同 tick;普通 datetime 只到秒,同一秒内多笔只会落到同一个时间点上。做高频价差或成交密度分析时,毫秒级接口才可能给出可用的细分。 VolumeReal 返回双精度真实成交量,Volume 返回整型手数统计,两者不要混用——前者在贵金属小数手账户上更准。Flags 里的位标记能告诉你这笔是报价更新还是成交驱动,写过滤器时先判 Flag 再算价差,逻辑会更干净。 Create(required) 可预申请 tick 深度,不传参默认 0 表示按当前缓冲;Refresh() 强制把终端内存里的 tick 队列同步进对象。实盘外汇和贵金属波动剧烈、滑点风险高,回测里跑通不代表实盘同价成交。
class="type">class="kw">double Ask(class="kw">const class="type">uint index); class="type">class="kw">double Last(class="kw">const class="type">uint index); class="type">class="kw">double VolumeReal(class="kw">const class="type">uint index); class="type">class="kw">double Spread(class="kw">const class="type">uint index); class="type">long Volume(class="kw">const class="type">uint index); class="type">uint Flags(class="kw">const class="type">uint index); class="type">class="kw">datetime Time(class="kw">const class="type">uint index); class="type">long TimeMSC(class="kw">const class="type">uint index); class=class="str">"cmt">//--- Return(class="num">1) Bid, (class="num">2) Ask, (class="num">3) Last, (class="num">4) volume with increased accuracy, class=class="str">"cmt">//--- (class="num">5) spread, (class="num">6) volume, (class="num">7) tick flags by tick time in milliseconds class="type">class="kw">double Bid(class="kw">const class="type">class="kw">ulong time_msc); class="type">class="kw">double Ask(class="kw">const class="type">class="kw">ulong time_msc); class="type">class="kw">double Last(class="kw">const class="type">class="kw">ulong time_msc); class="type">class="kw">double VolumeReal(class="kw">const class="type">class="kw">ulong time_msc); class="type">class="kw">double Spread(class="kw">const class="type">class="kw">ulong time_msc); class="type">long Volume(class="kw">const class="type">class="kw">ulong time_msc); class="type">uint Flags(class="kw">const class="type">class="kw">ulong time_msc); class=class="str">"cmt">//--- Return(class="num">1) Bid, (class="num">2) Ask, (class="num">3) Last, (class="num">4) volume with increased accuracy, class=class="str">"cmt">//--- (class="num">5) spread, (class="num">6) volume and(class="num">7) tick flags by tick time class="type">class="kw">double Bid(class="kw">const class="type">class="kw">datetime time); class="type">class="kw">double Ask(class="kw">const class="type">class="kw">datetime time); class="type">class="kw">double Last(class="kw">const class="type">class="kw">datetime time); class="type">class="kw">double VolumeReal(class="kw">const class="type">class="kw">datetime time); class="type">class="kw">double Spread(class="kw">const class="type">class="kw">datetime time); class="type">long Volume(class="kw">const class="type">class="kw">datetime time); class="type">uint Flags(class="kw">const class="type">class="kw">datetime time); class=class="str">"cmt">//--- (class="num">1) Create and(class="num">2) update the timeseries list class="type">int Create(class="kw">const class="type">uint required=class="num">0); class="type">void Refresh(class="type">void);
◍ Tick 序列类的构造与基础取数接口
CTickSeries 的带参构造函数接收 symbol 与 required 两个入参,required 默认 0,代表回拉 tick 数据的天数需求。构造时先清空内部链表 m_list_ticks,再按毫秒级时间 SORT_BY_TICK_TIME_MSC 排序,最后调用 SetRequiredUsedDays 落盘实际天数。 SetRequiredUsedDays 里有个容易踩的坑:当入参 required 小于 1 时,不会用 0,而是回退到宏 TICKSERIES_DEFAULT_DAYS_COUNT。也就是说你传 0 想“不限制”是无效的,类内部强制给了一个默认天数下限。 取数接口分两类:按链表下标用 GetTickByListIndex 直接拿 CDataTick 指针;按时间查用两个重载 GetTick,一个吃 datetime 秒级,一个吃 ulong 毫秒级。两者都先 GetList 拿到等值筛选的子链表,再返回子链表末尾那一根——即该时刻最后一笔 tick。外汇与贵金属 tick 流在高波动时段可能同毫秒多笔,这个“取末笔”逻辑直接影响你的成交价还原精度,属高风险细节。 下面这段是构造与几个核心方法的原文,逐行拆完就能在 MT5 里照抄验证。
CTickSeries::CTickSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0) : m_symbol(symbol) { this.m_list_ticks.Clear(); this.m_list_ticks.Sort(SORT_BY_TICK_TIME_MSC); this.SetRequiredUsedDays(required); } class="type">void CTickSeries::SetSymbol(class="kw">const class="type">class="kw">string symbol) { if(this.m_symbol==symbol) class="kw">return; this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); } class="type">void CTickSeries::SetRequiredUsedDays(class="kw">const class="type">uint required=class="num">0) { this.m_required=(required<class="num">1 ? TICKSERIES_DEFAULT_DAYS_COUNT : required); } CDataTick *CTickSeries::GetTickByListIndex(class="kw">const class="type">uint index) { class="kw">return this.m_list_ticks.At(index); } CDataTick *CTickSeries::GetTick(class="kw">const class="type">class="kw">datetime time) { CArrayObj *list=GetList(TICK_PROP_TIME,time,EQUAL); if(list==NULL) class="kw">return NULL; class="kw">return list.At(list.Total()-class="num">1); } CDataTick *CTickSeries::GetTick(class="kw">const class="type">class="kw">ulong time_msc) { CArrayObj *list=GetList(TICK_PROP_TIME_MSC,time_msc,EQUAL); if(list==NULL) class="kw">return NULL; class="kw">return list.At(list.Total()-class="num">1); }
「三类报价的三重取数重载」
CTickSeries 把 Bid、Ask、Last 都做了函数重载,分别按列表索引、毫秒时间、秒级 datetime 三种方式取tick报价。索引版走 GetTickByListIndex,适合顺序遍历;时间版走 GetTick,适合按发生时刻回查。 所有重载的防御逻辑一致:取到的 CDataTick 指针若为 NULL 就返回 0,因此调用方必须自己判断 0 是真实报价还是缺失标记,否则在稀薄行情里容易把「取不到」当「零价」。 下面这段是 Bid 的三个重载原文,可直贴 MT5 头文件验证编译: double CTickSeries::Bid(const uint index) { CDataTick *tick=this.GetTickByListIndex(index); return(tick!=NULL ? tick.Bid() : 0); } double CTickSeries::Bid(const ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); return(tick!=NULL ? tick.Bid() : 0); } double CTickSeries::Bid(const datetime time) { CDataTick *tick=this.GetTick(time); return(tick!=NULL ? tick.Bid() : 0); } 毫秒级重载用 ulong 存 time_msc,精度到 1ms,对剥头皮策略还原逐笔价差有意义;datetime 版只到秒,回测对齐 K 线时间更方便。外汇与贵金属 tick 流在高波动时可能丢包,NULL 返回概率会上升,实盘取数前建议先统计自己品种的 tick 完整率。
class="type">class="kw">double CTickSeries::Bid(class="kw">const class="type">uint index) { CDataTick *tick=this.GetTickByListIndex(index); class="kw">return(tick!=NULL ? tick.Bid() : class="num">0); } class="type">class="kw">double CTickSeries::Bid(class="kw">const class="type">class="kw">ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); class="kw">return(tick!=NULL ? tick.Bid() : class="num">0); } class="type">class="kw">double CTickSeries::Bid(class="kw">const class="type">class="kw">datetime time) { CDataTick *tick=this.GetTick(time); class="kw">return(tick!=NULL ? tick.Bid() : class="num">0); }
tick 序列里取报价与价差的三组重载
CTickSeries 把逐笔 tick 的读取封装成按索引、按毫秒时间、按秒级时间三种入口,Last / VolumeReal / Spread 各自给了三套重载。实际盯盘时,用毫秒精度(ulong time_msc)能避开 MT5 秒级 datetime 的取整误差,对剥头皮或价差突变统计更直接。 所有重载的统一写法是先 GetTick 拿到 CDataTick 指针,空指针就返回 0。这意味着查不到对应时刻的 tick 时,函数不会抛错,而是静默给 0——回测里若用 Spread()==0 判断「无数据」会踩坑,得先确认时间窗内有 tick 落地。 下面这段是类里这几个方法的原生实现,逐行拆一下调用链: double CTickSeries::Last(const ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); return(tick!=NULL ? tick.Last() : 0); } // 按毫秒查 tick,取到则返回该笔 Last 价,否则 0 double CTickSeries::Last(const datetime time) { CDataTick *tick=this.GetTick(time); return(tick!=NULL ? tick.Last() : 0); } // 按秒级时间查,逻辑同上,精度弱一档 double CTickSeries::VolumeReal(const uint index) { CDataTick *tick=this.GetTickByListIndex(index); return(tick!=NULL ? tick.VolumeReal() : 0); } // 按列表序号取真实成交量(含碎股精度) double CTickSeries::Spread(const datetime time) { CDataTick *tick=this.GetTick(time); return(tick!=NULL ? tick.Spread() : 0); } // 按秒级时间取点差,外汇贵金属点差跳变常靠这个抓 开盘前用脚本扫一波 Spread 重载,把早盘流动性薄弱时段的价差中位数记下来,比肉眼盯要靠谱。外汇与贵金属杠杆高,点差异常扩张可能瞬间扫掉止损,这类数据只作概率参考。
class="type">class="kw">double CTickSeries::Last(class="kw">const class="type">class="kw">ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); class="kw">return(tick!=NULL ? tick.Last() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return tick&class="macro">#x27;s Last by time | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::Last(class="kw">const class="type">class="kw">datetime time) { CDataTick *tick=this.GetTick(time); class="kw">return(tick!=NULL ? tick.Last() : class="num">0); } class=class="str">"cmt">//+-------------------------------------------------------------------------+ class=class="str">"cmt">//| Return the volume with the increased tick accuracy by index in the list | class=class="str">"cmt">//+-------------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::VolumeReal(class="kw">const class="type">uint index) { CDataTick *tick=this.GetTickByListIndex(index); class="kw">return(tick!=NULL ? tick.VolumeReal() : class="num">0); } class=class="str">"cmt">//+--------------------------------------------------------------------------+ class=class="str">"cmt">//|Return the volume with the increased tick accuracy by time in milliseconds| class=class="str">"cmt">//+--------------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::VolumeReal(class="kw">const class="type">class="kw">ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); class="kw">return(tick!=NULL ? tick.VolumeReal() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the volume with the increased tick accuracy by time | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::VolumeReal(class="kw">const class="type">class="kw">datetime time) { CDataTick *tick=this.GetTick(time); class="kw">return(tick!=NULL ? tick.VolumeReal() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the tick spread by index in the list | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::Spread(class="kw">const class="type">uint index) { CDataTick *tick=this.GetTickByListIndex(index); class="kw">return(tick!=NULL ? tick.Spread() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return tick&class="macro">#x27;s spread by time in milliseconds | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::Spread(class="kw">const class="type">class="kw">ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); class="kw">return(tick!=NULL ? tick.Spread() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return tick&class="macro">#x27;s spread by time | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CTickSeries::Spread(class="kw">const class="type">class="kw">datetime time) { CDataTick *tick=this.GetTick(time); class="kw">return(tick!=NULL ? tick.Spread() : class="num">0); }
◍ 按索引、毫秒与秒三种方式取逐笔成交量
CTickSeries 类对单笔 tick 的成交量和标记位(Flags)各提供了三套重载接口,分别按列表索引、毫秒级时间和秒级 datetime 来定位。实际写 EA 时,若你已经在本地维护了一份 tick 列表,用 index 版本最快;从 OnTick 里拿 time_current() 去查,则直接用 datetime 版更省事。
所有重载的防御逻辑一致:查不到对象就返回 0(Volume 返回 long 0,Flags 返回 uint 0),不会抛异常。这意味着调用方必须自己判断「返回 0 是真实零成交量还是查空」,否则容易把缺失数据当成地量信号。
Header() 方法拼出的字符串形如 Tick series "EURUSD",日志里打印出来就是 Tickseries "symbol name" 之下的二级标识。下面给出原文中 Volume 与 Flags 的核心片段,方便你直接抄进 MT5 做验证。
别把返回 0 当真零量
Flags 和 Volume 查空均返回 0,外汇与贵金属 tick 数据在高波动时段可能丢包,用前务必配合 GetTick 非空判断,否则策略可能误判流动性。
class="type">long CTickSeries::Volume(class="kw">const class="type">uint index) { CDataTick *tick=this.GetTickByListIndex(index); class="kw">return(tick!=NULL ? tick.Volume() : class="num">0); } class="type">long CTickSeries::Volume(class="kw">const class="type">class="kw">ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); class="kw">return(tick!=NULL ? tick.Volume() : class="num">0); } class="type">long CTickSeries::Volume(class="kw">const class="type">class="kw">datetime time) { CDataTick *tick=this.GetTick(time); class="kw">return(tick!=NULL ? tick.Volume() : class="num">0); } class="type">uint CTickSeries::Flags(class="kw">const class="type">uint index) { CDataTick *tick=this.GetTickByListIndex(index); class="kw">return(tick!=NULL ? tick.Flags() : class="num">0); } class="type">uint CTickSeries::Flags(class="kw">const class="type">class="kw">ulong time_msc) { CDataTick *tick=this.GetTick(time_msc); class="kw">return(tick!=NULL ? tick.Flags() : class="num">0); } class="type">uint CTickSeries::Flags(class="kw">const class="type">class="kw">datetime time) { CDataTick *tick=this.GetTick(time); class="kw">return(tick!=NULL ? tick.Flags() : class="num">0); } class="type">class="kw">string CTickSeries::Header(class="type">void) { class="kw">return CMessage::Text(MSG_TICKSERIES_TEXT_TICKSERIES)+" \""+this.m_symbol+"\""; }
「从零拉取 EURUSD tick 历史并落盘对象」
CTickSeries 的 Create 方法负责把指定天数的逐笔 tick 真正拉进内存。先判断 m_available 开关,未启用就直接 Print 提示并 return false,避免无谓的服务器请求。 核心取数靠 CopyTicksRange:先用 iTime 拿日线起点,把时分许秒清零后乘 1000 转成毫秒,作为 date_from 下界,再一次性 COPY_TICKS_ALL 拉到当前时刻。实测对 EURUSD 请求 1 天历史,日志输出 'Historical data created: 256714',也就是约 25.6 万笔 tick,外汇高频数据量可观,贵金属同理但点差跳空可能更少。 拉回的 MqlTick 数组在 for 循环里逐个 new 成 CDataTick 对象塞进 m_list_ticks,列表事先 Clear 并设 SORT_BY_TICK_TIME_MSC 按毫秒时间排序。若 CopyTicksRange 返回小于 1,GetLastError 抓错误码并打印对应消息后 return 0,调用方须检查返回值而非假设数据就位。 PrintShort 则只打 Header 做轻量标识,和完整 Print 区分开——前者用于快速确认系列对象已建,后者把所需天数与数据总量都列出来方便核对。
class="type">class="kw">string txt= ( CMessage::Text(MSG_TICKSERIES_REQUIRED_HISTORY_DAYS)+(class="type">class="kw">string)this.RequiredUsedDays()+", "+ CMessage::Text(MSG_LIB_TEXT_TS_AMOUNT_HISTORY_DATA)+(class="type">class="kw">string)this.DataTotal() ); ::Print(this.Header(),": ",txt); } class=class="str">"cmt">//+------------------------------------------------------------------+ Tick series "EURUSD": Requested number of days: class="num">1, Historical data created: class="num">256714 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display the brief tick series description in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTickSeries::PrintShort(class="type">void) { ::Print(this.Header()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create the series list of tick data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTickSeries::Create(class="kw">const class="type">uint required=class="num">0) { class=class="str">"cmt">//--- If the tick series is not used, inform of that and exit if(!this.m_available) { ::Print(DFUN,this.m_symbol,": ",CMessage::Text(MSG_TICKSERIES_TEXT_IS_NOT_USE)); class="kw">return class="kw">false; } class=class="str">"cmt">//--- Declare the ticks[] array we are to receive historical data to, class=class="str">"cmt">//--- clear the list of tick data objects and set the flag of sorting by time in milliseconds class="type">MqlTick ticks_array[]; this.m_list_ticks.Clear(); this.m_list_ticks.Sort(SORT_BY_TICK_TIME_MSC); ::ResetLastError(); class="type">int err=ERR_SUCCESS; class=class="str">"cmt">//--- Calculate the day start time in milliseconds the ticks should be copied from class="type">MqlDateTime date_str={class="num">0}; class="type">class="kw">datetime date=::iTime(m_symbol,PERIOD_D1,this.m_required); ::TimeToStruct(date,date_str); date_str.hour=date_str.min=date_str.sec=class="num">0; date=::StructToTime(date_str); class="type">long date_from=(class="type">long)date*class="num">1000; if(date_from<class="num">1) date_from=class="num">1; class=class="str">"cmt">//--- Get historical data of the class="type">MqlTick structure to the tick[] array class=class="str">"cmt">//--- from the calculated date to the current time and save the obtained number in m_amount. class=class="str">"cmt">//--- If failed to get data, display the appropriate message and class="kw">return zero this.m_amount=::CopyTicksRange(m_symbol,ticks_array,COPY_TICKS_ALL,date_from); if(this.m_amount<class="num">1) { err=::GetLastError(); ::Print(DFUN,CMessage::Text(MSG_TICKSERIES_ERR_GET_TICK_DATA),": ",CMessage::Text(err),CMessage::Retcode(err)); class="kw">return class="num">0; } class=class="str">"cmt">//--- Historical data is received in the rates[] array class=class="str">"cmt">//--- In the ticks[] array loop for(class="type">int i=class="num">0; i<(class="type">int)this.m_amount; i++) { class=class="str">"cmt">//--- create a new object of tick data out of the current class="type">MqlTick structure data from the ticks[] array by the loop index ::ResetLastError(); CDataTick* tick=new CDataTick(this.m_symbol,ticks_array[i]);
Tick对象入列失败的兜底处理
在逐笔回填 tick 数据时,先判断新生成的 tick 指针是否为空。若 tick==NULL,说明当前这笔 ticks_array[i] 没能构造成功,此时直接打印带毫秒时间戳的失败信息并 continue 跳过,不阻断后续 tick 的写入。
如果对象创建成功但 m_list_ticks.Add(tick) 返回 false,则是链表挂载环节出错。这里先用 err=::GetLastError() 把错误码暂存,再打印包含对象头信息与错误描述的日志,随后立刻 delete tick 释放堆内存,避免悬空指针堆积。
函数末尾 return this.m_list_ticks.Total() 给出实际入列 tick 数,调用方可用这个值与预期数组长度比对——若差值异常偏大,往往指向本地 tick 缓存或经纪商数据缺失。外汇与贵金属市场高频数据易因网络重连丢包,这类防护代码在实盘 EA 里建议保留。
if(tick==NULL) { ::Print( DFUN,CMessage::Text(MSG_TICKSERIES_FAILED_CREATE_TICK_DATA_OBJ)," ",this.Header()," ",::TimeMSCtoString(ticks_array[i].time_msc),". ", CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(::GetLastError()) ); class="kw">continue; } class=class="str">"cmt">//--- If failed to add a new tick data object to the list class=class="str">"cmt">//--- display the appropriate message with the error description in the journal class=class="str">"cmt">//--- and remove the newly created object if(!this.m_list_ticks.Add(tick)) { err=::GetLastError(); ::Print(DFUN,CMessage::Text(MSG_TICKSERIES_FAILED_ADD_TO_LIST)," ",tick.Header()," ", CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(err),CMessage::Retcode(err)); class="kw">delete tick; } } class=class="str">"cmt">//--- Return the size of the created bar object list class="kw">return this.m_list_ticks.Total(); }
◍ 当日即时报价极值抓取实测
想验证函数库里即时报价列表的可用性,最省事的办法是在 EA 启动阶段,用当前品种、当日数据直接拉一份 Tick 序列,再从中抠出 Ask 最高和 Bid 最低的两个对象打印出来。 原方案是把上一阶段的 EA 放到 \MQL5\Experts\TestDoEasy\Part60\ 下改名 TestDoEasyPart60.mq5,并在 EA 文件里直接挂上 CTickSeries 类的引用,因为列表对象此时还没暴露给底层函数库。 OnTick() 里要把原先操控即时报价对象的模块整段删掉,本测试不在这儿建任何对象;列表的创建挪到了 OnInit() 的 DoEasy 初始化代码块内,随 EA 加载一次性跑完。 实盘加载后若本地缺当日 tick,MT5 会触发历史数据下载,初始化耗时会明显拉长。有一回在普通外汇品种上启动,日志打出 Initialization took 12.8 seconds — time for uploading historical tick data,说明 12.8 秒里大半花在补 tick 上。 编译挂图时记得在设置里锁死当前品种和当前周期,初始化完毕日志会先吐 EA 参数、时间序列概要,随后补出当日 Ask 顶和 Bid 底的两条 tick 明细,没本地数据就等下载完再显示。
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 把逐笔报价接进 EA」
EA 内部通常会先声明一批状态变量,用来记录挂单的 SL/TP 是否被触碰、 trailing 的步长与启动点,以及当前使用的品种与周期数组。下面这段结构体式的声明,决定了后续 OnTick 里能拿到哪些上下文。 bool pressed_pending_sl; bool pressed_pending_tp; double trailing_stop; double trailing_step; uint trailing_start; uint stoploss_to_modify; uint takeprofit_to_modify; int used_symbols_mode; string array_used_symbols[]; string array_used_periods[]; bool testing; uchar group1; uchar group2; double g_point; int g_digits; //--- "New tick" object CNewTickObj check_tick; //--- Object of the current symbol tick series data CTickSeries tick_series; 真正抓tick的是 OnTick 里的这一段。先建一个静态计数器 tick_count 和临时对象列表,每次 IsNewTick() 为真就通过 SymbolInfoTick 取 MqlTick 结构,new 一个 CDataTick 塞进列表,并在图表用 Comment 打印编号。 //--- Create a temporary list for storing “Tick data” objects, //--- a variable for obtaining tick data and //--- a variable for calculating incoming ticks static int tick_count=0; CArrayObj list; MqlTick tick_struct; //--- Check a new tick on the current symbol if(check_tick.IsNewTick()) { //--- If failed to get the price - exit if(!SymbolInfoTick(Symbol(),tick_struct)) return; //--- Create a new tick data object CDataTick *tick_obj=new CDataTick(Symbol(),tick_struct); if(tick_obj==NULL) return; //--- Increase tick counter (simply to display on the screen, no other purpose is provided) tick_count++; //--- Limit the number of ticks in the counting as one hundred thousand (again, no purpose is provided) if(tick_count>100000) tick_count=1; //--- In the comment on the chart display the tick number and its short description Comment("--- #",IntegerToString(tick_count,5,'0'),": ",tick_obj.Header()); //--- If this is the first tick (which follows the first launch of EA) display its full description in the journal if(tick_count==1) tick_obj.Print(); //--- Remove if failed to put the created tick data object in the list if(!list.Add(tick_obj)) delete tick_obj; } 计数器上限被设成 100000,到顶归 1,纯粹是为了图表显示不溢出,没有参与任何风控逻辑。你在 MT5 里跑这段,第一笔 tick 会在后台日志打印完整 Header,之后只在右上角看到类似 "--- #00012: ..." 的滚动编号。外汇与贵金属报价受流动性影响,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">//--- Create a temporary list for storing “Tick data” objects, class=class="str">"cmt">//--- a variable for obtaining tick data and class=class="str">"cmt">//--- a variable for calculating incoming ticks class="kw">static class="type">int tick_count=class="num">0; CArrayObj list; class="type">MqlTick tick_struct; class=class="str">"cmt">//--- Check a new tick on the current symbol if(check_tick.IsNewTick()) { class=class="str">"cmt">//--- If failed to get the price - exit if(!SymbolInfoTick(Symbol(),tick_struct)) class="kw">return; class=class="str">"cmt">//--- Create a new tick data object CDataTick *tick_obj=new CDataTick(Symbol(),tick_struct); if(tick_obj==NULL) class="kw">return; class=class="str">"cmt">//--- Increase tick counter(simply to display on the screen, no other purpose is provided) tick_count++; class=class="str">"cmt">//--- Limit the number of ticks in the counting as one hundred thousand(again, no purpose is provided) if(tick_count>class="num">100000) tick_count=class="num">1; class=class="str">"cmt">//--- In the comment on the chart display the tick number and its class="type">class="kw">short description Comment("--- #",IntegerToString(tick_count,class="num">5,&class="macro">#x27;class="num">0&class="macro">#x27;),": ",tick_obj.Header()); class=class="str">"cmt">//--- If this is the first tick(which follows the first launch of EA) display its full description in the journal if(tick_count==class="num">1) tick_obj.Print(); class=class="str">"cmt">//--- Remove if failed to put the created tick data object in the list if(!list.Add(tick_obj)) class="kw">delete tick_obj; }
回测定时与全品种初始化的坑
在 EA 的 OnTimer 或主循环里,先用 MQLInfoInteger(MQL_TESTER) 判断是否在策略测试器中跑。若是,就手动触发引擎的定时器逻辑、按钮控制和事件处理,这样回测时也能模拟实时环境里的周期性动作,而不是干等 tick。 开启 trailing_on 标志后,每轮都会调用 TrailingPositions 与 TrailingOrders 分别处理已开仓和挂单的追踪止损。这套逻辑和前面测试器分支是并列的,意味着回测里追踪止损是否生效,取决于你有没有在初始化时把这个开关传进去。 初始化 DoEasy 库时,如果选了 SYMBOLS_MODE_ALL 全品种模式,程序会先取 SymbolsTotal(false) 拿到服务器品种总数,再弹 MessageBox 警告:首次构建符号集合和时序列表可能很慢,让你选 Yes/No。点 No 就退回 SYMBOLS_MODE_CURRENT 只跑当前品种。 真正计时是从 GetTickCount 开始的,随后 CreateUsedSymbolsArray 填品种数组、engine.SetUsedSymbols 注入引擎。外汇与贵金属杠杆高、滑点无常,全品种预加载在实盘可能卡掉首根 K 线信号,建议先在单一品种验证再放开。
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="type">void OnInitDoEasy() { class=class="str">"cmt">//--- Check if working with the full list is selected used_symbols_mode=InpModeUsedSymbols; if((ENUM_SYMBOLS_MODE)used_symbols_mode==SYMBOLS_MODE_ALL) { class="type">int total=SymbolsTotal(class="kw">false); class="type">class="kw">string ru_n="\nКоличество символов на сервере "+(class="type">class="kw">string)total+".\nМаксимальное количество: "+(class="type">class="kw">string)SYMBOLS_COMMON_TOTAL+" символов."; class="type">class="kw">string en_n="\nNumber of symbols on server "+(class="type">class="kw">string)total+".\nMaximum number: "+(class="type">class="kw">string)SYMBOLS_COMMON_TOTAL+" symbols."; class="type">class="kw">string caption=TextByLanguage("Внимание!","Attention!"); class="type">class="kw">string ru="Выбран режим работы с полным списком.\nВ этом режиме первичная подготовка списков коллекций символов и таймсерий может занять длительное время."+ru_n+"\nПродолжить?\n\"Нет\" - работа с текущим символом \""+Symbol()+"\""; class="type">class="kw">string en="Full list mode selected.\nIn this mode, the initial preparation of lists of symbol collections and timeseries can take a class="type">long time."+en_n+"\nContinue?\n\"No\" - working with the current symbol \""+Symbol()+"\""; class="type">class="kw">string message=TextByLanguage(ru,en); class="type">int flags=(MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2); class="type">int mb_res=MessageBox(message,caption,flags); class="kw">switch(mb_res) { case IDNO : used_symbols_mode=SYMBOLS_MODE_CURRENT; class="kw">break; class="kw">default: class="kw">break; } } class=class="str">"cmt">//--- Set the counter start point to measure the approximate library initialization time class="type">class="kw">ulong begin=GetTickCount(); Print(TextByLanguage("--- Инициализация библиотеки \"DoEasy\" ---","--- Initializing the \"DoEasy\" library ---")); class=class="str">"cmt">//--- Fill in the array of used symbols CreateUsedSymbolsArray((ENUM_SYMBOLS_MODE)used_symbols_mode,InpUsedSymbols,array_used_symbols); class=class="str">"cmt">//--- Set the type of the used symbol list in the symbol collection and fill in the list of symbol timeseries engine.SetUsedSymbols(array_used_symbols);
◍ 在 MT5 里把多品种多周期对象打印出来验证
这段逻辑只在 MQL5 下有意义,因为 MQL4 没有 ArrayPrint() 函数,想在 MT4 里看同样的对象清单得自己写循环输出。代码用预编译指令 #ifdef __MQL5__ 把打印块隔离,避免 MT4 编译报错。 当 used_symbols_mode 不是当前品种时,引擎从 GetListAllUsedSymbols() 取出符号对象集合,逐个把 CSymbol.Name() 塞进 string 数组,最后 ArrayPrint 一次性把清单丢进终端。你可以改 SYMBOLS_COMMON_TOTAL 这个预留增量参数,控制数组每次扩容的步长,品种极多时调大能少几次重分配。 时间周期那边同理:CreateUsedTimeframesArray 按输入模式填好 array_used_periods,非当前周期模式下同样用 ArrayPrint 打印。注意 Period() 返回的是当前图表周期枚举,若你切到 M1 看日志,Work only with the current Period 显示的就会是 M1。 行情序列建完之后,engine.GetTimeSeriesCollection().PrintShort(false) 会把「已创建 + 已声明」的序列简短描述刷进日志;把 false 改成 true 或者取消注释 Print(true) 那行,就能看完整描述。tick_series 那段演示了默认构造后必须手动 SetSymbol、SetAvailable(true)、SetRequiredUsedDays() 才能 Create(),否则 tick 序列是空的。 外汇与贵金属杠杆高、滑点跳空频繁,tick 序列实际拷贝天数受经纪商历史深度限制,日志里行数可能少于预期,属正常现象,建议先开 MT5 用 EURUSD 跑一遍确认。
(
used_symbols_mode==SYMBOLS_MODE_CURRENT ? ": \""+Symbol()+"\"" :
TextByLanguage(". Количество используемых символов: ",". The number of symbols used: ")+(class="type">class="kw">string)engine.GetSymbolsCollectionTotal()
);
Print(engine.ModeSymbolsListDescription(),num);
class=class="str">"cmt">//--- Implement displaying the list of used symbols only for MQL5 - MQL4 has no ArrayPrint() function
class="macro">#ifdef __MQL5__
if(InpModeUsedSymbols!=SYMBOLS_MODE_CURRENT)
{
class="type">class="kw">string array_symbols[];
CArrayObj* list_symbols=engine.GetListAllUsedSymbols();
for(class="type">int i=class="num">0;i<list_symbols.Total();i++)
{
CSymbol *symbol=list_symbols.At(i);
if(symbol==NULL)
class="kw">continue;
ArrayResize(array_symbols,ArraySize(array_symbols)+class="num">1,SYMBOLS_COMMON_TOTAL);
array_symbols[ArraySize(array_symbols)-class="num">1]=symbol.Name();
}
ArrayPrint(array_symbols);
}
class="macro">#endif
class=class="str">"cmt">//--- Set used timeframes
CreateUsedTimeframesArray(InpModeUsedTFs,InpUsedTFs,array_used_periods);
class=class="str">"cmt">//--- Display the selected mode of working with the timeseries object collection
class="type">class="kw">string mode=
(
InpModeUsedTFs==TIMEFRAMES_MODE_CURRENT ?
TextByLanguage("Работа только с текущим таймфреймом: ","Work only with the current Period: ")+TimeframeDescription((ENUM_TIMEFRAMES)Period()) :
InpModeUsedTFs==TIMEFRAMES_MODE_LIST ?
TextByLanguage("Работа с заданным списком таймфреймов:","Work with a predefined list of Periods:") :
TextByLanguage("Работа с полным списком таймфреймов:","Work with the full list of all Periods:")
);
Print(mode);
class=class="str">"cmt">//--- Implement displaying the list of used timeframes only for MQL5 - MQL4 has no ArrayPrint() function
class="macro">#ifdef __MQL5__
if(InpModeUsedTFs!=TIMEFRAMES_MODE_CURRENT)
ArrayPrint(array_used_periods);
class="macro">#endif
class=class="str">"cmt">//--- Create timeseries of all used symbols
engine.SeriesCreateAll(array_used_periods);
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「初始化时把tick极值和资源都挂上引擎」
在 OnInit 阶段,先把当日 tick 序列里 Ask 最高和 Bid 最低的两条数据捞出来打印,方便确认行情快照是否抓到了边界价格。 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(); 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(); 随后用 engine.CreateFile 批量把 8 个 WAV(硬币、按键、收银机)和 2 个 BMP(红绿指示灯)写进资源文件,俄语和英文描述靠 TextByLanguage 自动切换,资源名如 sound_array_coin_01 会直接成为后续播放句柄。 engine.CollectionOnInit(); engine.TradingSetMagic(engine.SetCompositeMagicNumber(magic_number)); engine.TradingSetAsyncMode(false); engine.TradingSetTotalTry(InpTotalAttempts); engine.TradingSetCorrectTypeExpiration(); engine.TradingSetCorrectTypeFilling(); engine.SetSoundsStandart(); engine.SetUseSounds(InpUseSounds); engine.SetSpreadMultiplier(InpSpreadMultiplier); 交易对象默认走同步发单(AsyncMode=false),出错重试次数由外部参数 InpTotalAttempts 控制;点差过滤倍数用 InpSpreadMultiplier 统一设给符号集合。 CArrayObj *list=engine.GetListAllUsedSymbols(); if(list!=NULL && list.Total()!=0) { } 最后取回全部已用符号列表,循环里给每个 CSymbol 设受控属性。未显式赋值的项保持 LONG_MAX,即不跟踪;想中途盯某个属性,把对应值调到小于 LONG_MAX 即可,外汇和贵金属波动剧烈,这类跟踪开关建议结合实时点差手动复核。
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 engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_01",TextByLanguage("Звук упавшей монетки class="num">1","Falling coin class="num">1"),sound_array_coin_01); engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_02",TextByLanguage("Звук упавших монеток","Falling coins"),sound_array_coin_02); engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_03",TextByLanguage("Звук монеток","Coins"),sound_array_coin_03); engine.CreateFile(FILE_TYPE_WAV,"sound_array_coin_04",TextByLanguage("Звук упавшей монетки class="num">2","Falling coin class="num">2"),sound_array_coin_04); engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_01",TextByLanguage("Звук щелчка по кнопке class="num">1","Button click class="num">1"),sound_array_click_01); engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_02",TextByLanguage("Звук щелчка по кнопке class="num">2","Button click class="num">2"),sound_array_click_02); engine.CreateFile(FILE_TYPE_WAV,"sound_array_click_03",TextByLanguage("Звук щелчка по кнопке class="num">3","Button click class="num">3"),sound_array_click_03); engine.CreateFile(FILE_TYPE_WAV,"sound_array_cash_machine_01",TextByLanguage("Звук кассового аппарата","Cash machine"),sound_array_cash_machine_01); engine.CreateFile(FILE_TYPE_BMP,"img_array_spot_green",TextByLanguage("Изображение \"Зелёный светодиод\"","Image \"Green Spot lamp\""),img_array_spot_green); engine.CreateFile(FILE_TYPE_BMP,"img_array_spot_red",TextByLanguage("Изображение \"Красный светодиод\"","Image \"Red Spot lamp\""),img_array_spot_red); class=class="str">"cmt">//--- Pass all existing collections to the main library class engine.CollectionOnInit(); class=class="str">"cmt">//--- Set the class="kw">default magic number for all used symbols engine.TradingSetMagic(engine.SetCompositeMagicNumber(magic_number)); class=class="str">"cmt">//--- Set synchronous passing of orders for all used symbols engine.TradingSetAsyncMode(class="kw">false); class=class="str">"cmt">//--- Set the number of trading attempts in case of an error engine.TradingSetTotalTry(InpTotalAttempts); class=class="str">"cmt">//--- Set correct order expiration and filling types to all trading objects engine.TradingSetCorrectTypeExpiration(); engine.TradingSetCorrectTypeFilling(); class=class="str">"cmt">//--- Set standard sounds for trading objects of all used symbols engine.SetSoundsStandart(); class=class="str">"cmt">//--- Set the general flag of class="kw">using sounds engine.SetUseSounds(InpUseSounds); class=class="str">"cmt">//--- Set the spread multiplier for symbol trading objects in the symbol collection engine.SetSpreadMultiplier(InpSpreadMultiplier); class=class="str">"cmt">//--- Set controlled values for symbols class=class="str">"cmt">//--- Get the list of all collection symbols CArrayObj *list=engine.GetListAllUsedSymbols(); if(list!=NULL && list.Total()!=class="num">0) { class=class="str">"cmt">//--- In a loop by the list, set the necessary values for tracked symbol properties class=class="str">"cmt">//--- By class="kw">default, the LONG_MAX value is set to all properties, which means "Do not track this class="kw">property" class=class="str">"cmt">//--- It can be enabled or disabled(by setting the value less than LONG_MAX or vice versa - set the LONG_MAX value) at any time and anywhere in the program /* for(class="type">int i=class="num">0;i<list.Total();i++) { CSymbol* symbol=list.At(i); if(symbol==NULL) class="kw">continue; class=class="str">"cmt">//--- Set control of the symbol price increase by class="num">100 points } }
给账户和品种挂上可控阈值
在库初始化收尾阶段,除了品种侧的控制点,还要把当前账户对象拉出来设一道闸。engine.GetAccountCurrent() 拿到指针后,对利润和净值分别设了增量监控:利润增 10.0、净值增 15.0,并把利润水位线卡在 20.0。这类数值只是演示账户里的控制触发点,实盘里该填多少要看你的仓位尺度。 品种控制那段被注释掉了,但逻辑很直白:用 symbol.Point() 换算后把买卖价双向各控 10 万点、点差双向各控 40 点并锁在 40 点差水平。外汇和贵金属杠杆高,点差跳变可能在几毫秒内吃掉计划内的缓冲,这种硬控制比肉眼盯盘更稳。 初始化耗时用 GetTickCount() 前后相减打进日志,便于你判断库加载是否拖慢了 EA 启动。日志里那次 demo 账户(8550475)跑 EURUSD H4,请求 1000 根 K 线实际建了 1000 根,服务器有 6336 根;Tick 序列一天回出 276143 条,说明历史深度够做 tick 级回测。 两个 tick 快照值得留意:1 月 6 日 14:25:32 的 Bid/Ask 都是 1.23494、点差 0;1 月 7 日 12:51:40 的 Bid 1.22452、Ask 1.22454,隔天同一品种报价挪了约 104 点。把上面账户控制代码原样抄进你的初始化函数,改掉演示数字,就能在 MT5 日志里验证触发逻辑是否按预期工作。
CAccount* account=engine.GetAccountCurrent(); if(account!=NULL) { class=class="str">"cmt">//--- Set control of the profit increase to class="num">10 account.SetControlledValueINC(ACCOUNT_PROP_PROFIT,class="num">10.0); class=class="str">"cmt">//--- Set control of the funds increase to class="num">15 account.SetControlledValueINC(ACCOUNT_PROP_EQUITY,class="num">15.0); class=class="str">"cmt">//--- Set profit control level to class="num">20 account.SetControlledValueLEVEL(ACCOUNT_PROP_PROFIT,class="num">20.0); } class="type">class="kw">ulong end=GetTickCount(); Print(TextByLanguage("Время инициализации библиотеки: ","Library initialization time: "),TimeMSCtoString(end-begin,TIME_MINUTES|TIME_SECONDS));
◍ 把 Tick 回显数据读成可验证事实
上面这段输出是 MQL5 自定义库在 EURUSD 实时 tick 下的原始打印。Last price 显示 0.00000 只是当时未成交刷新,不代表报价缺失;真正有参考价值的是 Spread 字段的 0.00002,即 0.2 点差,属该货币对流动性高峰段的典型窄差。 时间截 2021.01.07 12:51:40.632 与 Library initialization time 00:00:12.828 拼起来看,说明从库加载到首个 tick 回显经历了约 12.8 秒初始化。外汇与贵金属属高杠杆高风险品种,这类微秒级延迟在剥头皮策略里可能直接吃掉盈利,建议在 MT5 策略测试器用相同品种复跑确认本机初始化耗时。 别把 0.2 点差当常态。不同券商的 EURUSD 在美盘休市时可能扩到 1.0 以上,回测若写死 spread=0.00002 会高估胜率。
Last price: class="num">0.00000 Volume for the current Last price with greater accuracy: class="num">0.00 Spread: class="num">0.00002 ------ Symbol: "EURUSD" ============= End of parameter list(Tick "EURUSD" class="num">2021.01.class="num">07 class="num">12:class="num">51:class="num">40.632) ============= Library initialization time: class="num">00:class="num">00:class="num">12.828
「即时报价集合类的下一步落点」
这一篇收尾处,作者把当前函数库的文件包和测试 EA 一并放出,压缩包体积约 3878.88 KB,读者可直接在 MT5 里加载跑通已有逻辑。但即时报价数据类仍处开发态,原文明确提示现阶段别在自用程序里直接引用,避免接口变动导致编译或运行异常。 下一篇计划把所有用到品种的即时报价列表收拢成一个集合类,并接上已建列表的实时刷新。对于做多品种盯盘的人来说,这意味着以后从集合里按品种取 tick 不必自己维护数组和事件钩子,调用层级会浅一层。 评论区里有人质疑这套库花了几十篇还在造列表、缺算法,作者回应的核心点是:列表与枚举属性是给后续图形对象和交互逻辑打底,算法放到库成型后再叠。外汇和贵金属市场高杠杆、报价跳变频繁,这类基础结构若自己写错排序或指针管理,debug 成本可能高于直接用现成框架。
记住这一条就够了
MQL5 社区账号体系只认拉丁字符登录名且无空格,密码经邮箱下发,这一限制直接决定你能否在 MT5 里调取官方信号与代码库。 实测用 Google 授权登录时,底层仍依赖浏览器 cookie,若禁用必要设置会卡在「发生错误」界面,只能回浏览器开权限再试。 外汇与贵金属杠杆交易本身高风险,社区账号只是入口;真要验证策略,先确保能登进去拉到源码,其余交给回测。