开发回放系统  市场模拟(第 08 部分):锁定指标·进阶篇
📘

开发回放系统 市场模拟(第 08 部分):锁定指标·进阶篇

第 2/2 篇

「用共享内存和图表钩子搭回放骨架」

做行情回放工具,第一步是把进程间通信和图表标识先定义死。下面这组宏和联合体就是底层骨架:全局变量名 "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 掉再重绘。外汇与贵金属杠杆高、滑点随机,回放仅用于验证策略逻辑,实盘表现可能偏离。

MQL5 / C++
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 重播不代表未来波动,参数验证请在策略测试器孤立环境进行。

MQL5 / C++
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 再用同样掩码清理占用位。

MQL5 / C++
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 则强制显示数据窗口。外汇与贵金属回放涉及杠杆与滑点,属高风险验证环境,结论仅作概率性参考。

MQL5 / C++
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,回放区就是空白,这是高频踩坑点。

MQL5 / C++
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 里跑顺,再谈下一步。

常见问题

用共享内存存回放状态,配合图表钩子隔离事件;回放指标启动前先校验窗口句柄与初始化标志,避免误写实盘数据。
在指标头部设独立运行锁与周期白名单,非回放周期直接返回;图形对象加专属前缀,防止被外部脚本误删。
可以。小布能按你给的图表事件规则自动跑一遍对象重建流程,标出残留图形位和重建失败点,省去手动逐帧查。
参数走外部输入+运行时默认值兜底,关键开关(如速度、起点)存共享内存;改动后重初始化校验一次即可生效。
在 OnChartEvent 里按对象前缀批量清理再重建窗口;重建前先判空和释放旧资源,外汇贵金属回放属高风险验证,建议先模拟盘跑通。