开发回放系统(第 61 部分):玩转服务(二)·综合运用
「直接读按钮对象状态替代缓冲区轮询」
回放/模拟服务里最吃 CPU 的是 LoopEventOnTime 定期调用检查控制按钮状态的函数。原方案每次都从控制指标缓冲区读数据判断播放/暂停,其实 OBJ_BITMAP_LABEL 对象自身就带状态变量,直接读对象属性更轻。 在 C_DrawImage.mqh 第 184–186 行做微小改动后,对象的 OBJPROP_STATE 会随播放(1)/暂停(0)实时更新。这样服务层可以绕过缓冲区,用 ObjectGetInteger 直接取状态。 修改点落在 C_Replay.mqh:构造函数里初始化 m_IndControl 指向控制指标创建的对象名;UpdateIndicatorControl 中先验证 handler 有效(第40行)、内存位置匹配(第41行),匹配后若静态变量为真则从图表对象读 OBJ_BITMAP_LABEL 当前值,否则退回缓冲区读。 实测这不一定让单次调用快很多,但每次调用省下的操作数在高频定时触发下会累积。外汇/贵金属回测环境开 MT5 按上面改完,用 Profiler 看 LoopEventOnTime 占用,可能能看到轻微下降。 别把缓冲读取妖魔化 缓冲区读取并非天生慢,只是比读对象直接属性多几步间接操作。只在暂停切播放的瞬间才需要回退缓冲区同步位置,日常轮询走对象属性就够了。
class="num">174. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">175. class="type">void Paint(class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">uchar cView, class="kw">const class="type">int what) class="num">176. { class="num">177. class="num">178. if ((m_szRecName == NULL) || (what < class="num">0) || (what >= def_MaxImages)) class="kw">return; class="num">179. ReSizeImage(w, h, cView, what); class="num">180. ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_XDISTANCE, x); class="num">181. ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_YDISTANCE, y); class="num">182. if (ResourceCreate(m_szRecName, m_Pixels, w, h, class="num">0, class="num">0, class="num">0, COLOR_FORMAT_ARGB_NORMALIZE)) class="num">183. { class="num">184. ObjectSetString(GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, m_szRecName); class="num">185. ObjectSetString(GetInfoTerminal().ID, m_szObjName, OBJPROP_BMPFILE, what, m_szRecName); class="num">186. ObjectSetInteger(GetInfoTerminal().ID, m_szObjName, OBJPROP_STATE, what == class="num">1); class="num">187. ChartRedraw(GetInfoTerminal().ID); class="num">188. }
◍ 用自定义品种搭回放沙盘
做历史行情回放,第一步不是读文件,而是在 MT5 里造一个干净的交易品种。上面这段类构造里,先删掉同名自定义品种再重建,路径写成 Custom\%s,等于在自定义分组下开了一块独立沙盘,和真实账户品种彻底隔离。
重建后立刻把 SYMBOL_TRADE_TICK_SIZE、SYMBOL_TRADE_TICK_VALUE、SYMBOL_VOLUME_STEP 三个字段全置 0(见 140–142 行)。这一步很关键:回放引擎不靠真实点值算盈亏,置 0 能避免 MT5 在绘图或测试时因点值异常报错。
srand(GetTickCount()) 放在 136 行,用系统节拍数播种子。回放若涉及随机偏移或噪声模拟,每次启动序列不同,可防止多实例回放撞出完全一样的伪随机轨迹。外汇与贵金属回放仅用于策略验证,实盘仍属高风险,置 0 参数不代表任何收益暗示。
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 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; 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);
回放符号初始化与控件状态同步
在 MT5 自定义回放环境里,先得把仿真品种的属性钉死。代码 143–144 行给重放符号写描述、设精度为 8 位小数,这种细粒度通常是为了让 tick 级回放不被报价舍入吃掉细节;145 行 SymbolSelect 置 true,否则图表里根本选不到这个自定义品种。 紧接着 146–150 行把回放计数、指标句柄、播放模式全部归零或置为暂停态,控件内存里的位置标记用 eTriState 占位,等于告诉主循环「还没收到有效定位」。外汇与贵金属回放涉及高杠杆与滑点模拟,实盘映射结果偏差可能很大,仅作策略逻辑验证用。 下面这段 UpdateIndicatorControl 是控件轮询核心。40 行先拦掉无效句柄;41 行比对内存中的位置标记和当前 Position,不一致才进分支。47–48 行用 CopyBuffer 取 1 根缓冲值写进 Memory.dValue,这是把指标面板上的按钮状态读回 C++ 结构体的关键一步。 49–51 行根据 CtrlStatus 决定播放还是暂停:若状态不是三态,就把 Mode 切过去,并在确为 ePlay 时把 Position 同步为内存位置。这样图表上的「播放/暂停」点击才能驱动回放指针往前走,开 MT5 把这段塞进 EA 的 OnTimer 里就能看到控件响应。
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; } class="kw">inline class="type">void UpdateIndicatorControl(class="type">void) { class="kw">static class="type">bool bTest = class="kw">false; 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(bTest) m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == class="num">1 ? C_Controls::ePause : C_Controls::ePlay); else { 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((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) if(bTest = ((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
「把回放控制状态塞进联合体内存」
这段逻辑出现在回放指标初始化分支里,核心是把控盘对象的状态打包进一个 16+8 字节的联合体,再抛给图表自定义事件。 第 55、56 行分别把持仓枚举和模式强转 ushort 写进 _16b 数组的对应槽位;57、58 行在 _8b 的高两位写入 'D' 和 'M' 字符标记,相当于给内存块打了一个可读指纹。 [CODE]
- {
- m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
- m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
- m_IndControl.Memory._8b[7] = 'D';
- m_IndControl.Memory._8b[6] = 'M';
- EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
- bTest = false;
- }
- }
- //+------------------------------------------------------------------+
[/CODE]
逐行拆一下:55 行把当前仓位方向存到联合体前 16 位区的 eCtrlPosition 下标;56 行把运行模式转成无符号短整型塞进 eCtrlStatus 槽,避免符号位污染。57、58 行反向填字符,方便在内存观测器里肉眼区分块类型。59 行用 EventChartCustom 把整个 dValue(双精度视图)发到 IdReplay 标识的图表事件,参数 evCtrlReplayInit 告诉接收端这是初始化帧。60 行把测试开关关掉,防止重复触发。
在 MT5 里打开回放指标,把联合体定义成 _16b[2] 配 _8b[8] 共享 dValue,断点跑这段,能在自定义事件日志里看到 dValue 随 Mode 切换而变化。外汇与贵金属回放测试属高风险验证环境,实盘前需在模拟盘充分跑通。
class="num">54. { class="num">55. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; class="num">56. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; class="num">57. m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;; class="num">58. m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;; class="num">59. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, ""); class="num">60. bTest = class="kw">false; class="num">61. } class="num">62. } class="num">63. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 别急着下结论
前面几轮重构把 C_Controls.mqh 里的定义搬进 C_Replay.mqh,可读性确实上来了,但回放服务的实测耗时几乎没有波动——至少在我的 MT5 环境里没看到明显变化。 真正能拉开差距的改动看起来有点反直觉:恢复按预期节奏生成一分钟柱的逻辑,而非在循环里做多余判断。把 C_Replay.mqh 整文件读一遍就能发现,几处直接调用生成函数的写法比原先绕一层封装快出一截。 你完全可以自己开 MT5 挂上 Anexo.zip 里的工程,用策略测试器跑同一段历史,对比修改前后从加载到首根回放柱的耗时。外汇与贵金属模拟本身高风险,任何性能优化都不构成胜率保证,只是让你验证想法时少等几秒。