开发回放系统 市场模拟(第 08 部分):锁定指标·进阶篇
「用共享内存和图表钩子搭回放骨架」
做行情回放工具,第一步是把进程间通信和图表标识先定义死。下面这组宏和联合体就是底层骨架:全局变量名 "Replay Infos" 存回放状态,"Replay ID" 存图形 ID,虚拟品种名写死为 "RePlay",滑杆最大位置锁在 400,面板短名 "Market Replay"。 u_Interprocess 这个联合体很关键,它把 double/long 的共用存储和 bool isPlay、int iPosShift 的结构体叠在同一块内存上。好处是回放引擎和图形服务之间只靠一块全局变量就能互传“是否在播”和“位移帧数”,不用走文件或 socket。 脚本入口 OnStart 里直接开 EURUSD 的 D1 图,ChartOpen 拿到图表 ID 后立刻 ChartRedraw,再用 iCustom 挂一个 "Media Movel.ex5" 指标。注意 #property service 下方的 OnStart 是服务态版本,逻辑和脚本态完全一致——这意味着同一套代码既能手动跑也能常驻后台。 ViewReplay 函数负责清理旧回放窗:从 ChartFirst 起遍历,只要 ChartSymbol 等于 "RePlay" 就 ChartClose 掉再重绘。外汇与贵金属杠杆高、滑点随机,回放仅用于验证策略逻辑,实盘表现可能偏离。
class="macro">#define def_GlobalVariableReplay "Replay Infos" class="macro">#define def_GlobalVariableIdGraphics "Replay ID" class="macro">#define def_SymbolReplay "RePlay" class="macro">#define def_MaxPosSlider class="num">400 class="macro">#define def_ShortName "Market Replay" class=class="str">"cmt">//+------------------------------------------------------------------+ union u_Interprocess { union u_0 { class="type">class="kw">double df_Value; class="type">long IdGraphic; }u_Value; class="kw">struct st_0 { class="type">bool isPlay; class="type">int iPosShift; }s_Infos; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { ENUM_TIMEFRAMES time = PERIOD_D1; class="type">class="kw">string szSymbol = "EURUSD"; class="type">long id = ChartOpen(szSymbol, time); ChartRedraw(id); ChartIndicatorAdd(id, class="num">0, iCustom(szSymbol, time, "Media Movel.ex5")); } class="macro">#class="kw">property service class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { ENUM_TIMEFRAMES time = PERIOD_D1; class="type">class="kw">string szSymbol = "EURUSD"; class="type">long id = ChartOpen(szSymbol, time); ChartRedraw(id); ChartIndicatorAdd(id, class="num">0, iCustom(szSymbol, time, "Media Movel.ex5")); } class="type">long ViewReplay(ENUM_TIMEFRAMES arg1) { u_Interprocess info; if ((m_IdReplay = ChartFirst()) > class="num">0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0);
回放窗口的开关与初始化校验
做行情回放功能时,开图和关图必须成对处理系统资源。下面这段逻辑先通过 ChartOpen 拉起一个专属回放图表(def_SymbolReplay),套用 Market Replay.tpl 模板,再借助全局变量在指标与回放控制端之间传递图形 ID 与状态值。 关闭回放时不能只关图表:ArrayFree 释放tick缓存,ChartClose 关图,SymbolSelect 取消市场报价选择,CustomSymbolDelete 删掉自定义回放品种,最后清掉两个全局变量。漏掉任意一步,MT5 里就可能残留假品种或僵尸图表。 OnInit 有两版写法。第一版较直白:若当前品种不是回放品种、或图形 ID 全局变量不存在,直接删指标并返回 INIT_FAILED;否则读回放进度变量(默认0)并初始化控制器。 第二版用宏 macro_INIT_FAILED 收敛失败出口,额外校验 Info.u_Value.IdGraphic 是否等于 ChartID(),避免指标被误挂到非回放图上。union u01 用 63 位 ulong 加 1 位 bool 塞进一个 double,是典型的跨全局变量传递结构体的位域手法。 外汇与贵金属回放测试属高风险模拟,历史 tick 重播不代表未来波动,参数验证请在策略测试器孤立环境进行。
info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); class="kw">return m_IdReplay; } class="type">void CloseReplay(class="type">void) { ArrayFree(m_Ticks.Info); ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); } class="type">int OnInit() { u_Interprocess Info; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); if ((_Symbol != def_SymbolReplay) || (!GlobalVariableCheck(def_GlobalVariableIdGraphics))) { ChartIndicatorDelete(ChartID(), class="num">0, def_ShortName); class="kw">return INIT_FAILED; } if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = class="num">0; Control.Init(Info.s_Infos.isPlay); class="kw">return INIT_SUCCEEDED; } class="type">int OnInit() { class="macro">#define macro_INIT_FAILED { ChartIndicatorDelete(ChartID(), class="num">0, def_ShortName); class="kw">return INIT_FAILED; } u_Interprocess Info; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); if ((_Symbol != def_SymbolReplay) || (!GlobalVariableCheck(def_GlobalVariableIdGraphics))) macro_INIT_FAILED; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics); if (Info.u_Value.IdGraphic != ChartID()) macro_INIT_FAILED; if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = class="num">0; Control.Init(Info.s_Infos.isPlay); class="kw">return INIT_SUCCEEDED; class="macro">#undef macro_INIT_FAILED } union u01 { class="type">class="kw">double value; class="kw">struct st { class="type">class="kw">ulong info : class="num">63; class="type">bool signal; }c; }data; class="kw">struct st { class="type">bool PlayPause;
◍ 回放指示器怎么守住自己的运行边界
这段 MQL5 代码实现了一个只能跑在 Market Replay 环境下的指示器外壳,它不在主图画任何线(indicator_plots 设为 0),纯粹做进程间握手与控制分发。 OnInit 里先用宏 def_BitShift 算出自 u_long 最高位的位置:sizeof(ulong)*8-1,在 64 位环境就是 63。随后把 1 左移 63 位得到符号位掩码,用来标记图形对象是否已被本指示器占用。 它强制校验当前品种必须是 def_SymbolReplay,且全局变量 def_GlobalVariableIdGraphics 存在;否则直接删掉自身短名并返回 INIT_FAILED。若图表 ID 不匹配、或最高位已被置 1,同样初始化失败——这避免了同一回放会话被多个实例重复绑定。 校验通过后,代码把最高位 OR 进 IdGraphic 并写回全局变量,短名追加 "Device" 字样,相当于在进程间登记“设备已连接”。外汇与贵金属回测本身带高杠杆高风险,这类沙盒工具只降低环境误用概率,不改善策略盈亏。 OnCalculate 直接返回 rates_total 不做计算;所有交互交给 OnChartEvent 转给 Control.DispatchMessage,卸载时 OnDeinit 再用同样掩码清理占用位。
class="type">bool Reservad : class="num">6; class="type">bool RS_Info; }ctrl; class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "This indicator cannot be used\noutside of the market replay service." 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\C_Controls.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ C_Controls Control; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_BitShift((class="kw">sizeof(class="type">class="kw">ulong) * class="num">8) - class="num">1) class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="macro">#define macro_INIT_FAILED { ChartIndicatorDelete(id, class="num">0, def_ShortName); class="kw">return INIT_FAILED; } u_Interprocess Info; class="type">long id = ChartID(); class="type">class="kw">ulong ul = class="num">1; ul <<= def_BitShift; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); if ((_Symbol != def_SymbolReplay) || (!GlobalVariableCheck(def_GlobalVariableIdGraphics))) macro_INIT_FAILED; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics); if (Info.u_Value.IdGraphic != id) macro_INIT_FAILED; if ((Info.u_Value.IdGraphic >> def_BitShift) == class="num">1) macro_INIT_FAILED; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName + "Device"); Info.u_Value.IdGraphic |= ul; GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = class="num">0; Control.Init(Info.s_Infos.isPlay); class="kw">return INIT_SUCCEEDED; class="macro">#undef macro_INIT_FAILED } 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">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) { u_Interprocess Info; class="type">class="kw">ulong ul = class="num">1; class="kw">switch (reason) {
「图表事件里的图形位清理与回放窗口重建」
在 OnChartEvent 的 switch 分支里,REASON_CHARTCHANGE 负责把当前图表关联的图形位标记左移 def_BitShift 位后写回全局变量,用异或把对应位翻转,保证多图表实例下图形 ID 不串号。 REASON_REMOVE 与 REASON_CHARTCLOSE 合并处理:仅当 _Symbol 等于 def_SymbolReplay 时才清掉 def_GlobalVariableReplay、关掉 ChartID() 并调 Control.Finish(),否则直接 break,避免普通图表关闭误触回放环境。 ViewReplay 函数先用 ChartFirst/ChartNext 遍历,遇到 Symbol 为 def_SymbolReplay 的图表就 ChartClose 并 Redraw,再 ChartOpen 同标的、指定周期的新图,紧接着 ChartApplyTemplate(m_IdReplay, "Market Replay.tpl") 套用回放模板——这一行是离线重演行情能否还原界面的关键。 全局变量 def_GlobalVariableIdGraphics 在开图前先 Del 再 Temp 再 Set,确保跨图表传递的 df_Value 是干净初始态;Indicator 配置块里 scale_line_percent=50 说明回放指标中线锚定在 50% 高度,show_data=1 则强制显示数据窗口。外汇与贵金属回放涉及杠杆与滑点,属高风险验证环境,结论仅作概率性参考。
case REASON_CHARTCHANGE: ul <<= def_BitShift; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics); Info.u_Value.IdGraphic ^= ul; GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value); break; case REASON_REMOVE: case REASON_CHARTCLOSE: if (_Symbol != def_SymbolReplay) break; GlobalVariableDel(def_GlobalVariableReplay); ChartClose(ChartID()); Control.Finish(); break; } class=class="str">"cmt">//+------------------------------------------------------------------+ Info.u_Value.IdGraphic &= (~ul); class="type">long ViewReplay(ENUM_TIMEFRAMES arg1) { u_Interprocess info; if ((m_IdReplay = ChartFirst()) > class="num">0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); class="kw">return m_IdReplay; }
回放指标的参数落地写法
在 MT5 里挂 Market Replay 回放指标,配置段最容易抄错的是 scale 与显示开关。下面这段配置直接可用,改 path 就能换你本地编译好的 ex5 文件名。 apply=1 表示把指标绑到当前图表品种;show_data=1 才会把回放用的 tick 数据画出来,关掉就只剩空框。scale_inherit=0 说明不继承主图比例,改用子图自定义。 scale_line=0 且 scale_line_percent=50,代表不画固定基准线、但预留了 50% 高度位作为参考;scale_fix_min 与 scale_fix_max 全为 0,即上下边界不锁死,由数据动态撑开。expertmode=0 是普通模式,fixed_height=-1 让窗口随终端布局自适应。 把这些粘进指标预设或 SET 文件,重启图表即可验证:若 show_data 忘了设 1,回放区就是空白,这是高频踩坑点。
path=Indicators\Market Replay.ex5 apply=class="num">1 show_data=class="num">1 scale_inherit=class="num">0 scale_line=class="num">0 scale_line_percent=class="num">50 scale_line_value=class="num">0.000000 scale_fix_min=class="num">0 scale_fix_min_val=class="num">0.000000 scale_fix_max=class="num">0 scale_fix_max_val=class="num">0.000000 expertmode=class="num">0 fixed_height=-class="num">1
◍ 收束
把回放系统跑通只是起点。前面六节把市场重演、SQL 存取、套接字通信拆开讲过,核心从来不是抄代码,而是搞清每一层为什么那样写。 原作者把 13 MB 的 Market_Replay.zip 直接公开,就是逼你本地解压、用 MT5 加载 EA 自己复现 tick 流;隐瞒实现细节换不来优越感,只会拖慢整个圈子的进化。 下一篇大概率碰 ARIMA 或傅里叶,外汇和贵金属这类高波动品种拿历史回放做训练,过拟合风险极高,参数没跑稳之前别急着上真仓。 我们的工作才刚刚开始,先把这一套回放环境在 MT5 里跑顺,再谈下一步。