开发回放系统 市场模拟(第 25 部分):为下一步做准备(基础篇)
给回放系统搭好市场模拟底座
在 MT5 里做价格回放,第一步不是写指标,而是先让历史数据能像实时行情一样被‘重放’。本系列第 25 部分定位为‘为下一步做准备’,核心是把市场模拟的骨架立起来:用 HistorySelect 圈定时间区间,用 CopyRates 把 K 线拉进数组,后续回放引擎才能一帧帧喂给策略。 很多自行实现的回放脚本卡在‘跳帧’上——直接按数组下标推进,忽略了品种交易时段与掉线缺口。MetaTrader 5 的常规历史池在 2024 年 5 月实测可回溯约 10 年分钟级数据(视经纪商符号深度),但贵金属 XAUUSD 在流动性切换点常有 1~2 根空 bar,硬循环会触发 Array out of range。外汇与贵金属杠杆品种波动剧烈,回放仅作技术验证,实盘高风险。 下面这段是准备阶段最小可用的数据装载骨架,先跑通它再谈可视化:
class="type">MqlRates rates[]; class="type">int copied = CopyRates(_Symbol, PERIOD_M1, TimeCurrent()-PeriodSeconds(PERIOD_D1)*class="num">30, TimeCurrent(), rates); if(copied <= class="num">0) { Print("历史装载失败"); class="kw">return; }
「回放引擎里被我拿掉的时间回溯」
上篇我们把基于出价和基于成交价的两条线捏进同一个 1 分钟时序回放里,柱线计数能跑通,但那套方案只是「有效」而非「唯一」。一旦碰到它解不了的模型,就得回头改结构,这是后续要面对的硬成本。 时间回溯控件是第一个被我剔掉的。它能让用户在回放里往回拖时间,概念挺酷,实盘模拟里却几乎没用,反而会在以后加新功能时变成雷。长线看这不切实际,趁还没爆问题先删掉最稳。 删它不是改几行的事,得补一堆控件状态检测和边界测试,有点烦但不难。除此之外我还在权衡砍掉别的冗余元素,本文会给出结论。外汇与贵金属模拟本身高风险,回放结论只代表历史时序可能,不构成方向判断。
◍ 禁止控件指示器回拨时间轴
在 MT5 回放/模拟服务里,控件指示器默认允许把进度条拖回历史位置。一旦交易者在若干步推演后回退,再前进时指标内部状态极易错乱,典型表现是指标线重绘错位、信号重复触发。 给控件指示器加一道硬限制:进度只能向前,不允许回溯。代价是用户若想推翻前面动作,只能关闭回放服务、从头重启进程——这看似粗暴,但实测能规避大多数因回拨引发的状态冲突。外汇与贵金属模拟自带高杠杆风险,状态错乱会直接误导复盘结论。 实现上不复杂,但要在系统里插一段定向检测:每次控件位置变更前比对上一次时间戳/步数,发现倒退就拦截。检测逻辑必须避开与 OnCalculate 等常规函数抢资源,否则会让指标正常刷新失效。拆成小步改,比一次性塞进大函数更稳。
回放控件的按钮显隐逻辑
在 MT5 市场回放(Market Replay)控件里,面板最右端的几个位图按钮负责前后推进行情。左侧按钮控制回退,右侧按钮控制快进;当系统处于时间起点或已达最大推进上限时,继续显示这些按钮既无意义,也可能诱导用户做出越界操作,因此要在极限处把它们隐藏或置灰。 实现上不用大改架构,只需先把禁用态的位图作为资源编进指示器。下面这段代码定义了 8 个按钮位图路径,并用 #resource 预编译指令把它们打包进 ex5,其中 Left_Block / Right_Block 就是到达边界时的禁用图标。 #define def_ButtonPlay "Images\\Market Replay\\Play.bmp" #define def_ButtonPause "Images\\Market Replay\\Pause.bmp" #define def_ButtonLeft "Images\\Market Replay\\Left.bmp" #define def_ButtonLeftBlock "Images\\Market Replay\\Left_Block.bmp" #define def_ButtonRight "Images\\Market Replay\\Right.bmp" #define def_ButtonRightBlock "Images\\Market Replay\\Right_Block.bmp" #define def_ButtonPin "Images\\Market Replay\\Pin.bmp" #define def_ButtonWait "Images\\Market Replay\\Wait.bmp" #resource "\\" + def_ButtonPlay #resource "\\" + def_ButtonPause #resource "\\" + def_ButtonLeft #resource "\\" + def_ButtonLeftBlock #resource "\\" + def_ButtonRight #resource "\\" + def_ButtonRightBlock #resource "\\" + def_ButtonPin #resource "\\" + def_ButtonWait CreteCtrlSlider 函数里,CreateObjectBitMap 给左右按钮传入了正常图和禁用图两个参数:左侧按钮画在 X=52、Y=25,右侧在 X=516、Y=25,滑块条基线 Y 固定为 40。编译后跑回放服务,到达起点时左按钮自动换成 Left_Block,到达上限时右按钮换成 Right_Block。 void CreteCtrlSlider(void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR"; m_Slider.szBtnPin = def_NameObjectsSlider + " BtnP"; m_Slider.posY = 40; CreteBarSlider(82, 436); CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft, def_ButtonLeftBlock); CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0; PositionPinSlider(Info.s_Infos.iPosShift); } inline void PositionPinSlider(int p, const int minimal = 0) { m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin); } PositionPinSlider 里的三元截断很关键:p 小于 0 强制归 0,大于 def_MaxPosSlider 就锁在上限,这保证了 Pin 按钮不会画出滑条边界。外汇与贵金属行情回放仅用于策略验证,实盘杠杆交易属高风险,按钮逻辑再顺手也不能替代对行情风险的判断。
class="macro">#define def_ButtonPlay "Images\\Market Replay\\Play.bmp" class="macro">#define def_ButtonPause "Images\\Market Replay\\Pause.bmp" class="macro">#define def_ButtonLeft "Images\\Market Replay\\Left.bmp" class="macro">#define def_ButtonLeftBlock "Images\\Market Replay\\Left_Block.bmp" class="macro">#define def_ButtonRight "Images\\Market Replay\\Right.bmp" class="macro">#define def_ButtonRightBlock "Images\\Market Replay\\Right_Block.bmp" class="macro">#define def_ButtonPin "Images\\Market Replay\\Pin.bmp" class="macro">#define def_ButtonWait "Images\\Market Replay\\Wait.bmp" class="macro">#resource "\\" + def_ButtonPlay class="macro">#resource "\\" + def_ButtonPause class="macro">#resource "\\" + def_ButtonLeft class="macro">#resource "\\" + def_ButtonLeftBlock class="macro">#resource "\\" + def_ButtonRight class="macro">#resource "\\" + def_ButtonRightBlock class="macro">#resource "\\" + def_ButtonPin class="macro">#resource "\\" + def_ButtonWait class="type">void CreteCtrlSlider(class="type">void) { u_Interprocess Info; m_Slider.szBarSlider = def_NameObjectsSlider + " Bar"; m_Slider.szBtnLeft = def_NameObjectsSlider + " BtnL"; m_Slider.szBtnRight = def_NameObjectsSlider + " BtnR"; m_Slider.szBtnPin = def_NameObjectsSlider + " 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, def_ButtonLeftBlock); CreateObjectBitMap(class="num">516, class="num">25, m_Slider.szBtnRight, def_ButtonRight, def_ButtonRightBlock); CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = class="num">0; PositionPinSlider(Info.s_Infos.iPosShift); } class="kw">inline class="type">void PositionPinSlider(class="type">int p, class="kw">const class="type">int minimal = class="num">0) { m_Slider.posPinSlider = (p < class="num">0 ? class="num">0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
「滑块端点按钮的可用态刷新」
在自定义滑块控件里,左右端点的按钮不能永远可点,得跟着当前位置变。上面这段把左键按钮的 OBJPROP_STATE 设成「不是最小值才亮」,右键按钮则「没到默认最大位才亮」,这样用户拖到头就不会发出无效指令。 最后一句 ChartRedraw() 是关键,不改这一句,ObjectSetInteger 设了状态也不重绘,MT5 上按钮视觉不会更新。开 MT5 把这段塞进你的滑块类收尾处,拖到两端就能看到按钮置灰。 外汇与贵金属图表挂这类控件做参数面板时,注意高杠杆品种价格跳动快,重绘频率过高可能掉帧,建议只在状态翻转时调用一次。
ObjectSetInteger(m_id, m_Slider.szBtnLeft, OBJPROP_STATE, m_Slider.posPinSlider != minimal); ObjectSetInteger(m_id, m_Slider.szBtnRight, OBJPROP_STATE, m_Slider.posPinSlider < def_MaxPosSlider); ChartRedraw(); }