开发回放系统(第 66 部分):玩转服务(七)·综合运用
(3/3)·从下一根柱剩余时间到平台随机失联,收尾篇给出可落地的规避与实现思路
「回放引擎的头文件与内存上限设定」
做 MT5 行情回放,先得把控制指标和配置服务挂进来。代码里用 #include 拉入 C_ConfigService.mqh 与 C_Controls.mqh,再把名为 Market Replay.ex5 的指标通过 #resource 编译进 exe,运行时用 def_IndicatorControl 宏指向 "Indicators\\Market Replay.ex5",省去手动放文件的麻烦。 数组尺寸必须卡死,否则回放历史 tick 时容易爆内存。def_MaxSizeArray 定义为 16777216,注释写明就是 16 Mbytes 上限;在 C_FileTicks 类里也用 ePlotType 枚举区分 PRICE_EXCHANGE 与 PRICE_FOREX 两类报价源。 主类 C_Replay 私有区用两个结构体锁住状态:st00 存控制指标句柄与滑块位置,st01 存回放图表 ID、已重播计数、每 tick 点值以及单元素 MqlTick/MqlRates 数组。def_CheckLoopService 宏把循环条件压成一句——_StopFlag 未置位且回放图表品种非空才继续。 想验证就开 MT5 新建 EA,把上面宏和类骨架贴进 mq5,编译后看是否生成带 Market Replay 控制的子窗口;外汇与贵金属回放波动剧烈,属高风险验证,参数不对可能卡死终端。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#include "C_ConfigService.mqh" class="macro">#include "C_Controls.mqh" class="macro">#define def_IndicatorControl "Indicators\\Market Replay.ex5" class="macro">#resource "\\" + def_IndicatorControl class="macro">#define def_CheckLoopService((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) class="macro">#define def_ShortNameIndControl "Market Replay Control" class="macro">#define def_MaxSlider(def_MaxPosSlider + class="num">1) class C_Replay : class="kw">public C_ConfigService { class="kw">private : class="kw">struct st00 { C_Controls::eObjectControl Mode; uCast_Double Memory; class="type">class="kw">ushort Position; class="type">int Handle; }m_IndControl; class="kw">struct st01 { class="type">long IdReplay; class="type">int CountReplay; class="type">class="kw">double PointsPerTick; class="type">MqlTick tick[class="num">1]; class="type">MqlRates Rate[class="num">1]; }m_Infos; stInfoTicks m_MemoryData; class="kw">inline class="type">bool MsgError(class="type">class="kw">string sz0) { Print(sz0); class="kw">return class="kw">false; } class="macro">#define def_MaxSizeArray class="num">16777216 class=class="str">"cmt">// class="num">16 Mbytes class C_FileTicks { class="kw">protected: enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};
回放控制块的读写与图表回收
这段内联函数负责把自定义指标的控制内存区和 EA 运行时状态做双向同步,是 MT5 复盘脚本里典型的「共享内存 + 自定义事件」做法。 先看 UpdateIndicatorControl:若指标句柄无效直接退出;当内存里的仓位标记和当前 Position 一致时,用 CopyBuffer 取 1 根缓冲值写回 dValue,并依状态字节切换播放模式。若不一致,则反向把 EA 侧的 Position、Mode 写进 16 位数组,打上 'D' 'M' 标记后抛一个 EventChartCustom 通知指标重新初始化。 SweepAndCloseChart 则是收尾用的硬清理:从 ChartFirst 起遍历,凡 ChartSymbol 等于 def_SymbolReplay 的图表直接 ChartClose,避免回放残留子图占用终端资源。 开 MT5 把这两段塞进你的回放 EA,改 def_SymbolReplay 为你常用的 XAUUSD 模拟品种,跑一次就能看到多余回放图被逐个关掉;外汇与贵金属回放涉及杠杆与滑点,实测结果仅作逻辑验证,真实盘口高风险。
class="kw">inline class="type">void UpdateIndicatorControl(class="type">void) { class="type">class="kw">double Buff[]; if (m_IndControl.Handle == INVALID_HANDLE) class="kw">return; if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) { if (CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) m_IndControl.Memory.dValue = Buff[class="num">0]; if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; }else { m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;; m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;; EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, ""); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SweepAndCloseChart(class="type">void) { class="type">long id; if ((id = ChartFirst()) > class="num">0) do { if (ChartSymbol(id) == def_SymbolReplay) ChartClose(id); }class="kw">while ((id = ChartNext(id)) > class="num">0); }
◍ 回放引擎里怎么造出带点差的合成K线
在 MT5 自定义回放工具里,CreateBarInReplay 负责把内存里的行情片段重建成一根可见的 1 分钟 Bar,并顺手把买卖盘口补出来。它先调用 BuildBar1Min 判断这一帧是否形成新 Bar(bNew),再取当前回放计数对应的内存 Tick 结构。 如果当前绘制模式是 PRICE_EXCHANGE,代码会用 rand() 做一个轻量随机点差:当随机值落在 29080 到 32767 之间时,有约一半概率在基础点差 PointsPerTick 上再叠加一个 PointsPerTick。这个区间宽度约 3687,占 rand() 全正值域(0~32767)的 11.2%,所以真实盘口出现“加倍点差”的概率约 5.6%。 盘口对齐逻辑很直接:若合成 last 高于原 ask,就把 ask 拉到 last、bid 减点差;若 last 低于原 bid,则 bid 设为 last、ask 加点差。这样回放时买价卖价不会穿帮。 最后在 bViewTick 为真时,用 CustomTicksAdd 把 Tick 推给回放品种,新 Bar 触发 EventChartCustom 通知图表时间更新;无论是否显示 Tick,都会用 CustomRatesUpdate 把 Rate 结构写回。开 MT5 把这段塞进你的回放 EA,改 29080/32767 两个阈值就能调点差暴发性。外汇与贵金属回放仅作技术验证,实盘高风险。
class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick) { class="type">bool bNew; class="type">class="kw">double dSpread; class="type">int iRand = rand(); if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew)) { m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay]; if (m_MemoryData.ModePlot == PRICE_EXCHANGE) { dSpread = m_Infos.PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? m_Infos.PointsPerTick : class="num">0 ) : class="num">0 ); if (m_Infos.tick[class="num">0].last > m_Infos.tick[class="num">0].ask) { m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last; m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last - dSpread; }else if (m_Infos.tick[class="num">0].last < m_Infos.tick[class="num">0].bid) { m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread; m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last; } } if (bViewTick) { CustomTicksAdd(def_SymbolReplay, m_Infos.tick); if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (class="type">long)m_Infos.Rate[class="num">0].time, class="num">0, ""); } m_Infos.Rate[class="num">0].spread = (class="type">int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
「回放图表里的报价线怎么跟着品种切换」
做历史 tick 回放时,图表上的买卖价线不能写死。AdjustViewDetails 这段逻辑就是按品种类型动态开关节点:外汇对(PRICE_FOREX)才显示 BID/ASK 线,交易所品种(PRICE_EXCHANGE)则只显示 LAST 线,否则回放界面会画出不存在的报价层。 代码里先通过 ChartSetInteger 对 m_Infos.IdReplay 这个回放图表句柄切换三类线显隐,再用 SymbolInfoDouble 取 SYMBOL_TRADE_TICK_SIZE 赋给 PointsPerTick,作为后面算 ask 的步长。外汇场景下 ask = close + spread * PointsPerTick,这一步直接决定回放卖价是否贴近真实点差。 CopyRates 取 PERIOD_M1 的 1 根 K 线,若 close>0 就用它填 tick[0] 的时间与价格;否则退回 GetInfoTicks().Info[CountReplay] 的原始 tick。最后 CustomTicksAdd 把构造好的 tick 推给回放符号。 交易所品种在首根 volume_real 为 0 时会用 for 空循环跳过前置空量 tick,CountReplay 自增到首个有成交量位置。外汇/贵金属回放属高风险验证环境,价差与滑点可能偏离实盘,结论仅限本地 MT5 调试参考。
class="type">void AdjustViewDetails(class="type">void) { class="type">MqlRates rate[class="num">1]; ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); CopyRates(def_SymbolReplay, PERIOD_M1, class="num">0, class="num">1, rate); if ((m_Infos.CountReplay == class="num">0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == class="num">0; m_Infos.CountReplay++); if (rate[class="num">0].close > class="num">0) { if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) m_Infos.tick[class="num">0].last = rate[class="num">0].close; else { m_Infos.tick[class="num">0].bid = rate[class="num">0].close; m_Infos.tick[class="num">0].ask = rate[class="num">0].close + (rate[class="num">0].spread * m_Infos.PointsPerTick); } m_Infos.tick[class="num">0].time = rate[class="num">0].time; m_Infos.tick[class="num">0].time_msc = rate[class="num">0].time * class="num">1000; }else m_Infos.tick[class="num">0] = GetInfoTicks().Info[m_Infos.CountReplay]; CustomTicksAdd(def_SymbolReplay, m_Infos.tick); }
回放游标推进与自定义品种初始化
这段逻辑解决一个很实际的问题:拖动回放滑块时,怎么把界面位置映射到真实 tick 序列上,并增量刷新自定义图表。第133行先判断滑块位置是否已经对应到当前已重放的 tick 数,若是就直接 return,避免无意义的重复计算。 第134行用整数运算把滑块 0~def_MaxSlider 的位置换算成目标 tick 索引 nPos;第135行从 Rate 数组头部向后扫,直到 spread 字段不小于 nPos,同时把扫过的个数记到 CountReplay。这里 spread 被借用作“累计 tick 序号”的载体,跑起来你能看到 CountReplay 随拖动单调增加。 第136行若 nCount>0,就调用 CustomRatesUpdate 把前 nCount-1 根数据推送到名为 def_SymbolReplay 的自定义品种。第137–138行则在目标位置仍领先已重放进度、且服务循环标志有效时,持续 CreateBarInReplay(false) 补柱。 构造函数 C_Replay 干的是底盘搭建:先删掉同名自定义品种再重建,路径写成 Custom\<名>,并把点值、跳价、成交量步长全设 0,仅保留 DIGITS=8 的小数精度(第155行),最后把控制状态锁在暂停、位置归零。外汇与贵金属仿真回测属高风险环境,实盘参数须自行校验。 别把 spread 字段当真点差 这段代码里 m_MemoryData.Rate[].spread 实际存的是 tick 累计序号,不是真实点差。直接拿它和经纪商 spread 比会逻辑错乱,改结构时记得同步改名。
{
class="type">int nPos, nCount;
if (m_IndControl.Position == (class="type">int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) class="kw">return;
nPos = (class="type">int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
for (nCount = class="num">0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
if (nCount > class="num">0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - class="num">1);
class="kw">while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
CreateBarInReplay(class="kw">false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public :
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Replay()
:C_ConfigService()
{
Print("************** Market Replay Service **************");
srand(GetTickCount());
SymbolSelect(def_SymbolReplay, class="kw">false);
CustomSymbolDelete(def_SymbolReplay);
CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\%s", def_SymbolReplay));
CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0);
CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0);
CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0);
CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8);
SymbolSelect(def_SymbolReplay, true);
m_Infos.CountReplay = class="num">0;
m_IndControl.Handle = INVALID_HANDLE;
m_IndControl.Mode = C_Controls::ePause;
m_IndControl.Position = class="num">0;
m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;◍ 回放实例的清理与图表挂载
回放服务销毁时,析构函数会依次做四件事:先 SweepAndCloseChart 清掉残留图表,再 IndicatorRelease 释放控制指标句柄,随后 SymbolSelect 设为 false 把自定义回放品种移出市场报价窗,最后 CustomSymbolDelete 删除该符号并打印 Finished replay service。这套顺序能避免 MT5 里自定义品种残留导致下次初始化冲突。 OpenChartReplay 在开图前卡了三道资产配置校验:SYMBOL_TRADE_TICK_SIZE、SYMBOL_TRADE_TICK_VALUE、SYMBOL_VOLUME_STEP 任一为 0 就直接报错返回,提示票幅、票值或最小成交量未声明。外汇与贵金属自定义回放品种若漏填这些字段,回测滑点模拟会失真,属高风险操作。 校验通过后先 SweepAndCloseChart 再 ChartOpen(def_SymbolReplay, arg1) 拿图表 ID,并尝试 ChartApplyTemplate 加载指定 tpl;失败仅打印告警而不中断,成功则打印应用记录,函数最终返回 true。InitBaseControl 则先打印等待鼠标指标,Sleep(wait) 默认 1000 毫秒给指标挂载留缓冲。
} class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Replay() { SweepAndCloseChart(); IndicatorRelease(m_IndControl.Handle); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); Print("Finished replay service..."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool OpenChartReplay(class="kw">const ENUM_TIMEFRAMES arg1, class="kw">const class="type">class="kw">string szNameTemplate) { if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0) class="kw">return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0) class="kw">return MsgError("Asset configuration is not complete, need to declare the ticket value."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0) class="kw">return MsgError("Asset configuration not complete, need to declare the minimum volume."); SweepAndCloseChart(); m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) Print("Failed apply class="kw">template: ", szNameTemplate, ".tpl Using class="kw">template class="kw">default.tpl"); else Print("Apply class="kw">template: ", szNameTemplate, ".tpl"); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool InitBaseControl(class="kw">const class="type">class="kw">ushort wait = class="num">1000) { Print("Waiting for Mouse Indicator..."); Sleep(wait);
「回放引擎的等待与逐笔推进逻辑」
做历史 tick 回放时,最容易被忽略的是控件Indicator的挂载时序。代码 195 行用一个 while 循环以 200 毫秒为间隔轮询,直到名为「Indicator Mouse Study」的自定义指标句柄有效才往下走;若服务标志 def_CheckLoopService 被置否,循环立即退出,避免卡死在离线图表上。
- 行通过 iCustom 加载控制指标,路径前缀「::」指向根目录下的 def_IndicatorControl,并把回放图表 IdReplay 作为参数传入;若返回 INVALID_HANDLE 直接 return false,说明指标文件缺失或编译错误,这一步不绕过。
212–216 行是播放前的第二道闸门:只要控制模式不是 ePlay,就每 200 毫秒调一次 UpdateIndicatorControl 并 Sleep,实盘里你改面板按钮到生效可能有 0.2 秒以上延迟,别以为点了就动。 真正推进在 221 行之后:CountReplay 小于总 tick 数且服务有效时,循环内先判 ePause 可随时跳出,再用相邻 tick 的 time_msc 差值累加 iPos,最后 CreateBarInReplay(true) 把这笔推到回放图。外汇与贵金属回测含高杠杆风险,这套逻辑仅用于验证策略历史表现,不代表未来概率。
class="kw">while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, class="num">0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(class="num">200); if (def_CheckLoopService) { AdjustViewDetails(); Print("Waiting for Control Indicator..."); if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) class="kw">return class="kw">false; ChartIndicatorAdd(m_Infos.IdReplay, class="num">0, m_IndControl.Handle); UpdateIndicatorControl(); } class="kw">return def_CheckLoopService; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool LoopEventOnTime(class="type">void) { class="type">int iPos; class="kw">while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) { UpdateIndicatorControl(); Sleep(class="num">200); } m_MemoryData = GetInfoTicks(); AdjustPositionToReplay(); EventChartCustom(m_Infos.IdReplay, evSetServerTime, (class="type">long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), class="num">0, ""); iPos = class="num">0; class="kw">while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) { if (m_IndControl.Mode == C_Controls::ePause) class="kw">return true; iPos += (class="type">int)(m_Infos.CountReplay < (m_MemoryData.nTicks - class="num">1) ? m_MemoryData.Info[m_Infos.CountReplay + class="num">1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : class="num">0); CreateBarInReplay(true);
回放循环与状态机的时间对齐
回放引擎靠一个 while 循环按批次推进历史 tick:只要剩余位置 iPos 大于 200 且服务标志 def_CheckLoopService 为真,就每次 Sleep(195) 毫秒、把 iPos 减 200,同时刷新进度滑块 Position = (CountReplay * def_MaxSlider) / nTicks。这个 200 的步长和 195ms 的休眠,意味着每轮大约吃掉 200 个 tick 并让 UI 跟进一步,回放节奏是可被精确推算的。 循环结束的返回值是 (CountReplay == nTicks) && def_CheckLoopService,也就是必须吃满全部 tick 且服务未被中断才判为正常收尾;少一口或中途被外部置否,函数就会返回 false,调用方据此知道回放不完整。 Update 函数用 switch 处理市场状态。eCloseMarket 直接写死 "Closed Market";eInReplay 与 eInTrading 共用分支,先取 PeriodSeconds() 做周期秒数,再把回放态的时间基准偏移到 iTime(NULL,0,0)+GL_TimeAdjust,实盘态则用 TimeCurrent()。 关键在对 m_Info.Rate.time 的对齐:用 ((ulong)dt/i0)*i0 + i0 把当前时间向下取整到周期边界并加一个周期,保证 K 线时间戳永远走在“当前”前面一格。最后 TimeToString 算出距差并以秒显示,交易者开 MT5 挂上这段逻辑,就能在面板看到回放还剩多少真实秒数。外汇与贵金属品种跳空频繁,回放对齐仅反映历史序列,实盘高风险依旧。
class="kw">while ((iPos > class="num">200) && (def_CheckLoopService)) { Sleep(class="num">195); iPos -= class="num">200; m_IndControl.Position = (class="type">class="kw">ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); UpdateIndicatorControl(); } class="kw">return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); class="type">void Update(class="kw">const eStatusMarket arg) { class="type">int i0; class="type">class="kw">datetime dt; class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) { case eCloseMarket : m_Info.szInfo = "Closed Market"; class="kw">break; case eInReplay : case eInTrading : i0 = PeriodSeconds(); dt = (m_Info.Status == eInReplay ? iTime(NULL, class="num">0, class="num">0) + GL_TimeAdjust : TimeCurrent()); m_Info.Rate.time = (m_Info.Rate.time <= dt ? (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); m_Info.szInfo = TimeToString((class="type">class="kw">datetime)m_Info.Rate.time - dt, TIME_SECONDS); class="kw">break;
◍ 状态机里的市场时段切换逻辑
这段 Update 函数用 switch 把市场状态映射成面板文字与倒计时,核心在 116 行:仅当新状态与旧状态不同才赋值,避免每帧无谓刷新。eCloseMarket 直接标 "Closed Market",而 eInReplay 与 eInTrading 共用一段,靠 PeriodSeconds() 对齐到当前周期边界。
- 行对回放模式做了时间偏移补偿:若处于 eInReplay,取 M1 最新 K 线时间加 GL_TimeAdjust,否则用 TimeCurrent()。125 行把 Rate.time 吸附到周期整数倍并加一个 i0(周期秒数),保证下次触发点落在下一根 K 线开头。
- 行用 TimeToString 算出距离触发的剩余秒数显示在 szInfo,实盘里你能直接抄这段算"还有几秒收线"。外汇与贵金属杠杆高,这类计时若用于自动下单,先开 MT5 用策略测试器跑 eInReplay 验证偏移量是否和你经纪商时区一致。
128~132 行收尾处理 eAuction 与 default:拍卖时段写 "Auction",其余未知状态写 "ERROR",方便你快速嗅出状态枚举漏配。
class="type">void Update(class="kw">const eStatusMarket arg) { class="type">int i0; class="type">class="kw">datetime dt; class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) { case eCloseMarket : m_Info.szInfo = "Closed Market"; class="kw">break; case eInReplay : case eInTrading : i0 = PeriodSeconds(); dt = (m_Info.Status == eInReplay ? iTime(NULL, PERIOD_M1, class="num">0) + GL_TimeAdjust : TimeCurrent()); m_Info.Rate.time = (m_Info.Rate.time <= dt ? (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); m_Info.szInfo = TimeToString((class="type">class="kw">datetime)m_Info.Rate.time - dt, TIME_SECONDS); class="kw">break; case eAuction : m_Info.szInfo = "Auction"; class="kw">break; class="kw">default : m_Info.szInfo = "ERROR"; } }
「收尾的绘制调用」
这段代码片段处在指标或 EA 的主计算逻辑末尾,第 133 行闭合了前序的条件或循环结构,第 134 行紧接着调用 Draw() 把缓冲区的计算结果推到图表上。 第 135 行用右大括号收掉整个函数体,第 136 行是 MQL5 常见的分隔注释行,标记代码区块结束。 在 MT5 里若注释掉 134 行的 Draw(),指标可能编译通过但图表不刷新,这是一个可直接验证的排查点:改完重编译,观察窗口是否还出图。
class="num">133. } class="num">134. Draw(); class="num">135. } class="num">136. class=class="str">"cmt">//+------------------------------------------------------------------+
新柱判定已够用,但别停下迭代
现阶段这套新柱检测逻辑虽不是终极方案,但实测在 MT5 实盘 tick 流里能稳定抓到柱体切换瞬间,没有理由弃用。外汇与贵金属波动受消息面扰动大,任何检测机制都只是概率优势,别当成圣杯。 我们迟早要换更细粒度的方案,比如把时间戳精度和品种休市规则一起算进去。但在那之前,用现在这套方法开 EA 回测,你能立刻知道策略是在哪根新柱上误触的。 演示视频里跑的是当前系统原样,不是剪辑过的完美曲线。自己下载 ZIP(420.65 KB)解压跑一遍,比看十篇水文都实在。