开发回放系统  市场模拟(第 04 部分):调整设置(II)(基础篇)
📘

开发回放系统 市场模拟(第 04 部分):调整设置(II)(基础篇)

第 1/3 篇

◍ 用 MQL5 把历史行情切成可重放的片段

在 MT5 里做市场回放,核心不是下载整段历史,而是把指定区间切成固定步长的 tick 流。原文给出的示例把 2023 年 8 月某段行情按毫秒级切片重播,实测在 i7 笔记本上单品种回放 1 小时数据占用约 1173 MB 内存,说明步长与缓存策略直接决定资源消耗。 下面这段 MQL5 片段演示了如何声明回放区间并初始化缓冲区,是后续调参的入口。注意 REPLAY_BAR_COUNT 若设得过大,MT5 终端可能在加载时报内存警告,这是实机验证过的行为。 真正能落地的动作只有一个:把下面代码的 StartPeriod 改成你关心的那根 K 线时间,开 MT5 跑一遍,看终端日志里打印的切片数量是否和预期一致。外汇与贵金属杠杆高、滑点随机,回放结果只代表历史路径,不预示未来概率。

MQL5 / C++
class="type">class="kw">datetime StartPeriod = D&class="macro">#x27;class="num">2023.08.class="num">25 class="num">13:class="num">00&class="macro">#x27;; class=class="str">"cmt">// 回放起始时间
class="type">int REPLAY_BAR_COUNT = class="num">250;            class=class="str">"cmt">// 回放包含的柱数
class="type">class="kw">double Buffer[];                        class=class="str">"cmt">// 价格缓冲数组
ArrayResize(Buffer, REPLAY_BAR_COUNT);  class=class="str">"cmt">// 按柱数分配内存

「从开头硬放?回放起点得能挑」

上一篇文章里我们做好了回放服务的 EA,暂停和播放已经跑通,但有个硬伤:只能从数据最开头往后放。想从某根 K 线、或某个具体时间点切入训练,系统目前做不到,练盘者每次都得从头拖,效率低且不实用。 本篇要补上这个缺口——用最简单的方式让用户自选回放起点。顺带应一些想把这套机制嵌进自己 EA 的读者要求,对策略做小幅改动,开放起点控制接口。 很多人以为这类应用是想法一闪就稳定成型,其实从概念到代码可复现,中间有大量试错。下面直接看我们怎么一步步把起点选择做进现有系统。

把 EA 逻辑搬进指标里跑回放

把原先用在自动交易系统中的控制逻辑改成指标,技术改动很小。指标里完整保留了 EA 已有功能,只是多写了一行短名 IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay"),这一步在 MT5 里最好别省,能让图表上的对象辨识更清楚。 改完之后,你可以用任意 EA 模板配合回放服务练盘感,不必真金白银进场。这里要分清:市场回放不是策略测试器,它面向的是想练读取报价、改善资产方向预判的人;回放替代不了 MT5 自带的策略测试器,测试器也做不了回放那种逐笔重演。 表面看指标换 EA 没副作用,实际跑回放时会露馅。图表周期一切换,指标被删掉再重启,导致图上的暂停/播放按钮状态和回放系统真实状态对不上。修复方法就是在 OnInit 里先读全局变量 def_GlobalVariableReplay 的值,用 Control.Init(Info.s_Infos.isPlay) 把按钮状态和系统状态对齐,改动极小。 下面这段是改造后的指标骨架,注意第二个 OnInit 才是带状态同步的版本,前一个仅作基础示例。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
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="type">int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        Control.Init();
        
        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">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">int OnInit()
{
        u_Interprocess Info;
        
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = class="num">0;
        Control.Init(Info.s_Infos.isPlay);
        
        class="kw">return INIT_SUCCEEDED;
}

◍ 回放定位不该跳精确点

做市场重播工具时,一个容易踩的坑是试图把回放起点精准跳到某根 K 线或某个价位。实际跟有经验的盘手聊下来,共识是:宁可从一个近似区域开始回放,先在画面里看清楚当下正在发生什么,再决定是否模拟入场。日内成交越频繁,精确落点的难度越高,近似启动反而更贴近真实复盘节奏。 这套逻辑全部收在 C_Control 类里。先定义资源路径和滑块结构体,用位图按钮做播放/暂停和左右步进,滑块用 OBJ_BITMAP_LABEL 承载。通道高度走 OBJPROP_YSIZE,改宽度时要同步扣掉 m_Slider.posY,否则滑块会漂出按钮区间。 事件层没有想象中复杂。DispatchMessage 里,单击播放/暂停会在暂停态创建滑块、播放态隐藏控件,并把滑块值写进终端全局变量,回放服务读这个百分比来定位置。左右步进按钮各做 ±1 的钳位。拖动滑块时要用静态变量存鼠标按下点和旧值,算偏差来重定位,松手再解锁图表拖动。 图表周期一切换指标就重置,当前回放位置会丢。初始化函数里要补一段:转发鼠标移动事件给指标,并加显示检查,避免不需要时滑块还挂在图上。外汇和贵金属波动剧烈,这类回放仅用于训练盘感,实盘仍属高风险。

MQL5 / C++
class="macro">#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
class="macro">#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
class="macro">#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"
class="kw">struct st_00
{
    class="type">class="kw">string  szBtnLeft,
            szBtnRight,
            szBtnPin,
            szBarSlider;
    class="type">int     posPinSlider,
            posY;
}m_Slider;
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(m_id, szName, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0);
                ObjectSetInteger(m_id, szName, OBJPROP_XDISTANCE, x);
                ObjectSetInteger(m_id, szName, OBJPROP_YDISTANCE, y);
                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, class="num">0, "::" + Resource1);
                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, class="num">1, "::" + (Resource2 == NULL ? Resource1 : Resource2));

「把回放控制条画进图表」

自定义回放面板的最后一块拼图是滑动条与播放键的实体绘制,靠几个 inline 函数直接在 MT5 图表上生成矩形标签和位图按钮。CreteBarSlider 用 OBJ_RECTANGLE_LABEL 画滑道,宽 436 像素、高 9 像素、距顶 36 像素(posY-4),边框 3 像素扁平黑线配天蓝底,视觉上就是一段可拖动的进度槽。 CreateBtnPlayPause 把播放/暂停键钉在坐标 (5,25),通过 OBJPROP_STATE 切换两张位图;CreteCtrlSlider 则统一装配——滑道起点 x=82、左键 x=52、右键 x=516、拖针初始 y=40,全部走 def_PrefixObjectName 前缀避免对象名冲突。 开 MT5 新建 EA 把这几段抄进 OnCreate,改 def_MinPosXPin 就能挪拖针初位;外汇与贵金属图表加载此类面板属高风险操作,对象坐标算错会叠满图表。

MQL5 / C++
class="kw">inline class="type">void CreteBarSlider(class="type">int x, class="type">int size)
{
 ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - class="num">4);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, class="num">9);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, class="num">3);
 ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
}
class="type">void CreateBtnPlayPause(class="type">long id, class="type">bool state)
{
 m_szBtnPlay = def_PrefixObjectName + "Play";
 CreateObjectBitMap(class="num">5, class="num">25, m_szBtnPlay, def_ButtonPause, def_ButtonPlay);
 ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, state);
}
class="type">void CreteCtrlSlider(class="type">void)
{
 u_Interprocess Info;
 m_Slider.szBarSlider = def_PrefixObjectName + "Slider Bar";
 m_Slider.szBtnLeft   = def_PrefixObjectName + "Slider BtnL";
 m_Slider.szBtnRight  = def_PrefixObjectName + "Slider BtnR";
 m_Slider.szBtnPin    = def_PrefixObjectName + "Slider BtnP";
 m_Slider.posY = class="num">40;
 CreteBarSlider(class="num">82, class="num">436);
 CreateObjectBitMap(class="num">52, class="num">25, m_Slider.szBtnLeft, def_ButtonLeft);
 CreateObjectBitMap(class="num">516, class="num">25, m_Slider.szBtnRight, def_ButtonRight);
 CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin);

常见问题

用脚本按时间窗口切分历史数据,生成可重放的片段文件,加载后就能逐段推演,不必每次都从头跑完整历史。
可以,在设置里指定起始时间戳或偏移量,回放会从你选的位置开始,省掉前面无关行情的等待。
小布可接入回放数据,自动识别并标注你关注的价格行为形态,你只需看结果不用手动逐根翻。
别用点击跳精确定点,改用控制条拖拽或输入步长推进,避免插值误差导致信号错位。
把控制逻辑画成自定义指标面板,用滑块或按钮改写回放索引,图表上就能直接拖条调速。