开发回放系统(第 51 部分):事情变得复杂(三)(基础篇)
「回放系统里开始冒出的复杂点」
这一篇进入回放系统开发的第 51 部分,作者 Daniel Jose 在 2025 年 1 月 10 日发布,原文页面显示阅读量 553、评论 0,说明这套系列已经写到很深的位置却还没引起大规模讨论。 回放系统的初衷是把历史报价按可控速度重演,方便手动复盘价格行为。但到第 51 部分,事情开始变复杂:单纯按时间戳推 Tick 已经不够,需要同时处理离线历史补全、不同品种点值切换和图形对象的状态同步。 对做贵金属和外汇复盘的交易者来说,这类底层复杂度直接决定回放是否会出现‘跳 K’或‘重复画对象’。MT5 自带的 Strategy Tester 可视化模式能部分验证,但自定义回放更要小心时钟与事件队列的耦合。
回放系统改版的真实动机
上一阶段我们给回放/模拟器服务加了自定义模板支持,好处很直接:你在回放里调好的模板,能原样搬到真实账户上用,研究和实盘不再割裂。但这暴露了老架构的短板——控制指标和用户交互别扭,系统里多处逻辑重复,更麻烦的是第一阶段开发时没防着有人用非预期方式戳系统,安全和稳定性其实有窟窿。 现在定位从纯开发转向给研究者用,安全水位必须拉起来。我们已经让控制指标脱离自由摆放,改由服务直接调用,不再允许随便挂到任意图表。冷静翻了一遍代码,单改指标不够,回放系统其他模块也得动,有些模块后续会开放给用户,所以你得清楚哪些能碰、哪些碰了会让系统抖。 本文不堆大功能,先讲清楚要改什么、为什么改。外汇和贵金属自带高杠杆高风险,这类回放工具只辅助验证策略,别当实盘信号源。代码不稳之前,后面的大改全悬空,这一步模糊但必须走。
◍ 把鼠标交互拆给独立模块
控制指标早期版本把所有逻辑都写死在自己体内,导致和系统的其他部分状态对不上。第一个该动的刀子,是把鼠标相关的一切挪到专门的鼠标指标模块里——这样以后任何新代码都不必再为鼠标事件做单独测试和解析,模块自己兜住。 看 OnInit 函数第 22 行:control 的声明和旧版完全不同。这一行干了兩件事——把控制指标注册成一个可被其他模块调用的子模块;同时声明控制指标不再自己解析鼠标,而是去向鼠标指标询问用户当前在做什么、准备做什么,再决定执行什么流程。让鼠标模块独占交互层,能避免「在 A 处修好、在 B 处引出新 bug」的互相干扰,两者并行不悖。 继承上也做了调整:C_Control 改为 C_Terminal 的私有继承者(见代码第 31 行),不再靠指针访问终端类,部分编译路径下批量访问会快一点。构造函数(第 130 行起)目前只允许服务加载本指标,并强制每图单实例;第 133 行把鼠标指标指针存进私有全局变量,后面才用得上。 一个容易踩的 MT5 细节:ChartRedraw 以往不传参,这里却传了图表 ID。原因是服务用 ChartOpen 开的图,其 ID 与用户手动加载指标时 ChartID 返回的 ID 不是同一个。若对象用 ObjectCreate 放置,服务开的图必须用 ChartOpen 的 ID,用户加载的图必须用 ChartID 的 ID,错配则对象不显示。代码第 157 行起已删掉鼠标移动/点击的解析事件,改由鼠标指标统一收口。 外汇与贵金属品种波动剧烈、杠杆风险高,回放模拟系统的模块拆分仅用于本地验证,不构成任何实盘方向暗示。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property icon "/Images/Market Replay/Icons/Replay - Device.ico" class="macro">#class="kw">property description "Control indicator for the Replay-Simulator service." class="macro">#class="kw">property description "This one doesn&class="macro">#x27;t work without the service loaded." class="macro">#class="kw">property version "class="num">1.51" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Market Replay\Service Graphics\C_Controls.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ C_Controls *control = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">long user00 = class="num">0; class=class="str">"cmt">//ID class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { u_Interprocess Info; ResetLastError(); if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", "Indicator Mouse Study")) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS)
「控制指标初始化与图表事件的分发落点」
这段逻辑展示了一个控制类指标在 MT5 下的典型生命周期:初始化失败直接返回 INIT_FAILED,成功则读取全局变量并广播等待状态解除事件。第 29 行用 GlobalVariableCheck 判断重放变量是否存在,不存在就给 df_Value 赋 0,避免后续计算引用到未定义全局变量。 OnCalculate 里用 static bool bWait 做边沿检测,只在 isWait 由 false 变 true 时发 ev_WaitOn,由 true 变 false 时发 ev_WaitOff 并回传 df_Value。这样图表端不会每根 K 线重复弹等待提示,只在实际状态翻转时响应一次。 OnChartEvent 只有一行:把 id、lparam、dparam、sparam 全转给 control 的 DispatchMessage。也就是说所有按钮、滑块等 GUI 交互都由控制对象内部消化,指标主文件保持极薄。 开 MT5 把下面代码贴进自定义指标,挂上 EURUSD 图表观察:切模板触发 REASON_TEMPLATE 时,控制对象是否按预期走析构。外汇与贵金属杠杆高,这类控制逻辑只解决状态同步,不暗示任何收益倾向。
{
Print("Control indicator failed on initialization.");
class="kw">return INIT_FAILED;
}
if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.df_Value = class="num">0;
EventChartCustom(user00, C_Controls::ev_WaitOff, class="num">1, Info.df_Value, "");
(*control).Init(Info.s_Infos.isPlay);
class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
{
class="kw">static class="type">bool bWait = false;
u_Interprocess Info;
Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
if (!bWait)
{
if (Info.s_Infos.isWait)
{
EventChartCustom(user00, C_Controls::ev_WaitOn, class="num">1, class="num">0, "");
bWait = true;
}
}else if (!Info.s_Infos.isWait)
{
EventChartCustom(user00, C_Controls::ev_WaitOff, class="num">1, Info.df_Value, "");
bWait = false;
}
class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
(*control).DispatchMessage(id, lparam, dparam, sparam);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
class="kw">switch (reason)
{
case REASON_TEMPLATE:回放面板的退出清理与按钮资源定义
在回放/仿真系统关闭时,代码通过 Print 输出提示并依据不同退出原因做分支处理。当触发参数变更、指标移除或图表关闭(REASON_PARAMETERS / REASON_REMOVE / REASON_CHARTCLOSE)且当前图表品种不等于回放专用品种 def_SymbolReplay 时,直接 break 跳出,不做清理。 若确为回放图表,则调用 GlobalVariableDel(def_GlobalVariableReplay) 删除跨进程全局变量,并用 ChartClose(user00) 关闭该图表,随后释放 control 对象指针(delete control),避免 MT5 终端残留孤儿资源。 回放控制面板的图形按钮走位图资源方案:def_PathBMP 指向 Images\Market Replay\Control\,Play.bmp、Pause.bmp、Left.bmp 等 8 个 BMP 通过 #resource 编译进 ex5。def_PrefixObjectName 设为 "Market Replay _ ",滑块对象名由它拼接 "Slider" 得到,确保多图表下对象 ID 不撞车。 开 MT5 验证时,把 Auxiliar\Interprocess.mqh 放到对应相对路径,否则 #include 会在编译期报找不到文件;按钮位图缺失则面板只显示空白位图框。
class="num">68. Print("Modified class="kw">template. Replay/simulation system shutting down."); class="num">69. case REASON_PARAMETERS: class="num">70. case REASON_REMOVE: class="num">71. case REASON_CHARTCLOSE: class="num">72. if (ChartSymbol(user00) != def_SymbolReplay) class="kw">break; class="num">73. GlobalVariableDel(def_GlobalVariableReplay); class="num">74. ChartClose(user00); class="num">75. class="kw">break; class="num">76. } class="num">77. class="kw">delete control; class="num">78. } class="num">79. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">002. class="macro">#class="kw">property copyright "Daniel Jose" class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">004. class="macro">#include "..\Auxiliar\Interprocess.mqh" class="num">005. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">006. class="macro">#define def_PathBMP "Images\\Market Replay\\Control\\" class="num">007. class="macro">#define def_ButtonPlay def_PathBMP + "Play.bmp" class="num">008. class="macro">#define def_ButtonPause def_PathBMP + "Pause.bmp" class="num">009. class="macro">#define def_ButtonLeft def_PathBMP + "Left.bmp" class="num">010. class="macro">#define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" class="num">011. class="macro">#define def_ButtonRight def_PathBMP + "Right.bmp" class="num">012. class="macro">#define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp" class="num">013. class="macro">#define def_ButtonPin def_PathBMP + "Pin.bmp" class="num">014. class="macro">#define def_ButtonWait def_PathBMP + "Wait.bmp" class="num">015. class="macro">#resource "\\" + def_ButtonPlay class="num">016. class="macro">#resource "\\" + def_ButtonPause class="num">017. class="macro">#resource "\\" + def_ButtonLeft class="num">018. class="macro">#resource "\\" + def_ButtonLeftBlock class="num">019. class="macro">#resource "\\" + def_ButtonRight class="num">020. class="macro">#resource "\\" + def_ButtonRightBlock class="num">021. class="macro">#resource "\\" + def_ButtonPin class="num">022. class="macro">#resource "\\" + def_ButtonWait class="num">023. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">024. class="macro">#define def_PrefixObjectName "Market Replay _ " class="num">025. class="macro">#define def_NameObjectsSlider def_PrefixObjectName + "Slider"
◍ 控件基类的私有布局与位图注入
这一节把图表交互控件的底层骨架收口:C_Controls 以私有继承方式复用 C_Terminal,宏 def_PosXObjects 被硬编码为 120,意味着所有派生控件默认左边距固定在 120 像素,改这里就能整体平移面板。 类内用结构体 st_00 把滑块相关资源名(左右按钮、图钉、滑条及其遮挡块)和位置字段打包进 m_Slider,posPinSlider、posY、Minimal 三个 int 控制了图钉停靠点与最小触发阈值,调参时直接动这个实例即可。 CreateObjectBitMap 是实际往图表塞图的关键:第 54 行用 OBJ_BITMAP_LABEL 建位图标签,第 55 行把 X 距离写成 def_PosXObjects + x,也就是 120 加偏移,第 56 行单独设 Y 距离。开 MT5 把这段抄进 EA,改 x/y 就能验证按钮会不会精确落在你想要的角落。外汇与贵金属图表挂这类自定义控件时波动跳点频繁,误触概率偏高,建议先在模拟盘跑通再上实盘。
class="macro">#define def_PosXObjects class="num">120 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\Auxiliar\C_Terminal.mqh" class="macro">#include "..\Auxiliar\C_Mouse.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Controls : class="kw">private C_Terminal { class="kw">protected: enum EventCustom {ev_WaitOn, ev_WaitOff}; class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string m_szBtnPlay; class="type">bool m_bWait; class="kw">struct st_00 { class="type">class="kw">string szBtnLeft, szBtnRight, szBtnPin, szBarSlider, szBarSliderBlock; class="type">int posPinSlider, posY, Minimal; }m_Slider; C_Mouse *m_MousePtr; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void CreateObjectBitMap(class="type">int x, class="type">int y, class="type">class="kw">string szName, class="type">class="kw">string Resource1, class="type">class="kw">string Resource2 = NULL) { ObjectCreate(GetInfoTerminal().ID, szName, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, def_PosXObjects + x); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, y);