开发回放系统 市场模拟(第 07 部分):首次改进(II)·进阶篇
(2/3)· 图表周期一切换指标就掉?用 DeInit 原因码和单例约束堵住回放服务的暗坑
◍ 回放控件的播放与滑块联动
这段逻辑处理图表事件里的对象点击,核心是播放按钮和回放滑块的状态同步。当 sparam 等于播放按钮名 m_szBtnPlay 时,先读按钮的 OBJPROP_STATE 赋给 isPlay,决定是否处于回放状态。 若 isPlay 为 false,调用 CreteCtrlSlider() 创建滑块控件;若为 true,则 RemoveCtrlSlider() 移除滑块并把 szBtnPin 置 NULL,避免界面残留。 随后把滑块位置 m_Slider.posPinSlider 写入 iPosShift,并用 GlobalVariableSet 把当前 Info.Value 存进 def_GlobalVariableReplay 全局变量,最后 ChartRedraw() 重绘。开 MT5 把这段塞进 CHARTEVENT_OBJECT_CLICK 分支,点播放键就能看到滑块随全局变量值切换显隐。 在外接回放系统里,全局变量 def_GlobalVariableReplay 通常承载 0~1 的进度比例,按钮状态翻转时滑块显隐切换延迟一般在 1 个图表事件周期内完成,外汇与贵金属品种因点差跳变可能让重绘略有滞后,属正常概率现象。
if(sparam==m_szBtnPlay) { Info.s_Infos.isPlay=(class="type">bool)ObjectGetInteger(m_id,m_szBtnPlay,OBJPROP_STATE); if(!Info.s_Infos.isPlay) CreteCtrlSlider(); else { RemoveCtrlSlider(); m_Slider.szBtnPin=NULL; } Info.s_Infos.iPosShift=m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay,Info.Value); ChartRedraw(); }
滑块拖拽与鼠标命中区的判定逻辑
在自定义滑块控件的事件分支里,左/右按钮的点击直接驱动锚点位置增减 1 个步长:命中 szBtnLeft 就 PositionPinSlider(posPinSlider - 1),命中 szBtnRight 则 +1,这是最轻量的步进交互。
鼠标移动事件(CHARTEVENT_MOUSE_MOVE)下,先把 lparam/dparam 转成 int 型的 x、y 坐标,再用 posPinSlider + def_MinPosXPin ± 14 算出锚点按钮的横向命中边界 px1/px2,纵向则锁在 posY ± 14 的 28 像素带内。
只有当鼠标左键按住(sparam 的 0x01 位为 1)且滑块按钮句柄非空时,才进入拖拽判定;首次落入命中区会记录 six=x 与 sps=当前位置,并关掉图表鼠标滚动避免冲突。之后 six>0 时每帧执行 PositionPinSlider(sps + x - six),实现跟手偏移。外汇与贵金属图表挂这类控件需注意:高频鼠标事件可能拖累老机型 MT5 的帧率,建议实机测拖拽延迟。
else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - class="num">1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + class="num">1); class="kw">break; case CHARTEVENT_MOUSE_MOVE: x =(class="type">int)lparam; y =(class="type">int)dparam; px1 = m_Slider.posPinSlider + def_MinPosXPin - class="num">14; px2 = m_Slider.posPinSlider + def_MinPosXPin + class="num">14; if ((((class="type">uint)sparam & 0x01) == class="num">1) && (m_Slider.szBtnPin != NULL)) { if ((y >= (m_Slider.posY - class="num">14)) && (y <= (m_Slider.posY + class="num">14)) && (x >= px1) && (x <= px2) && (six ==-class="num">1)) { class="num">6 = x; sps = m_Slider.posPinSlider; ChartSetInteger(m_id, CHART_MOUSE_SCROLL, false); } if (six > class="num">0) PositionPinSlider(sps + x - six); }else if (class="num">6 > class="num">0) {
「鼠标滚轮接管与计数器归位」
这段片段出现在自定义图表事件处理末尾,先把局部计数器 6 置为 -1,意味着本轮交互状态已结清,避免后续误触发。 紧接着用 ChartSetInteger 对当前图表 m_id 打开 CHART_MOUSE_SCROLL 开关,等于把滚轮缩放控制权交还给 MT5 原生逻辑,用户又能用滚轮自由拉伸周期。 最后 break 跳出事件分支。开 MT5 按 F4 把这段塞进 OnChartEvent 的末尾,可验证:滚轮在自定义面板操作后确实恢复默认可用,而不是卡死在脚本接管态。
class="num">6 =-class="num">1; ChartSetInteger(m_id, CHART_MOUSE_SCROLL, true); } class="kw">break; }
◍ 回放图表只留一张
自动开图的回放系统跑久了,最容易出的毛病就是同一个品种被反复开图,交易者根本分不清当前盯的是哪一张。更麻烦的是,若多张图都指向同一回放品种,全局终端变量里存的控制值会各图不一,指标逻辑可能互相打架。
下面的函数从源头掐掉这个问题:遍历终端已开图表,只要发现是回放品种就直接关掉,最后重开一张并挂上模板。这样一来,回放期间全局变量数值的稳定性至少有了基本保障。
需要留意,代码选择的是「遇图即关」而非「关一张就跳出循环」。若用后者,同一资产仍可能残留多张图;全关再重开虽然粗暴,但能保证干净。
不过这套逻辑并不阻止你之后手动给回放品种开新图。作者刻意没加「全程仅允许一张图」的硬限制,因为有些交易者就爱同品种多周期对照。替代做法是:禁止控制指标在非服务创建的图表上加载,谁要强行替换原图上的指标,图表一关回放服务就停。
[CODE]
long ViewReplay(ENUM_TIMEFRAMES arg1)
{
if ((m_IdReplay = ChartFirst()) > 0) do
{
if(ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay);
}while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
ChartRedraw(m_IdReplay);
return m_IdReplay;
}
[/CODE]
中文逐行拆解:
long ViewReplay(ENUM_TIMEFRAMES arg1) —— 定义函数,接收周期参数 arg1,返回图表 ID。
if ((m_IdReplay = ChartFirst()) > 0) do —— 取第一张图 ID 存入变量,若大于 0 表示有图,进入 do 循环。
if(ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); —— 若当前图品种等于回放品种常量,关闭该图。
}while ((m_IdReplay = ChartNext(m_IdReplay)) > 0); —— 取下一张图 ID,大于 0 继续循环,直到遍历完。
m_IdReplay = ChartOpen(def_SymbolReplay, arg1); —— 用指定周期重开回放品种图表,记录 ID。
ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); —— 给新图套用回放模板,控制指标随之加载。
ChartRedraw(m_IdReplay); —— 强制重绘,立即显示。
return m_IdReplay; —— 返回新图表 ID 供后续调用。
开 MT5 把这段塞进 EA,跑回放前先调用一次,能看到同名图表数量从 N 张收敛到 1 张;外汇与贵金属回放波动剧烈,多图变量冲突可能引发错单,建议先模拟盘验证。
class="type">long ViewReplay(ENUM_TIMEFRAMES arg1) { if ((m_IdReplay = ChartFirst()) > class="num">0) do { if(ChartSymbol(m_IdReplay) == def_SymbolReplay) ChartClose(m_IdReplay); }class="kw">while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); class="kw">return m_IdReplay; }
用全局变量锁住单图表控制权
想在 MT5 一个终端会话里只允许一个图表挂“市场回放”控制指标,核心是利用终端全局变量做跨图表互斥。结构体内布尔位很省,双精度 8 字节里 isPlay 只占 1 位,还剩 7 个空闲位,拿其中一个当 IsUsing 标记,就能知道任意已开图表上是否已有控制指标。 OnInit 里先查全局变量 Replay Infos:存在就读值,不存在就置 0。注意第一次写全局变量必须显式调用创建,否则平台不会把它当成会话级临时变量,重启或保存时值会丢。下面这段代码是指标初始化与原子的关键部分: [CODE] int OnInit() { u_Interprocess Info; IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay"); if(GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0; Control.Init(Info.s_Infos.isPlay); return INIT_SUCCEEDED; } #define def_GlobalVariableReplay "Replay Infos" #define def_MaxPosSlider 400 union u_Interprocess { double Value; struct st_0 { bool isPlay; bool IsUsing; int iPosShift; }s_Infos; }; void Init(const bool state = false) { u_Interprocess Info; if (m_szBtnPlay != NULL) return; m_id = ChartID(); } [/CODE] 逐行拆解:第 4 行给指标短名“Market Replay”,方便在窗口列表里手动删;第 5 行检查全局变量,有则取双精度值(里面塞着结构体位域),无则清 0;第 6 行把 isPlay 状态交给 Control 初始化;union 那段说明 8 字节 Value 和结构体共享内存,IsUsing 就是拿来占空闲位的互斥旗标。 坑在切换时间帧:MT5 改周期会先删后重建指标,若全局变量还报 IsUsing=true,重建时 OnInit 会拒绝挂载。解法是 OnDeInit 里读 reason,如果是 REASON_CHARTCHANGE,就把 IsUsing 位清掉再写回全局变量。读写的微小时差理论上有竞态,但回放系统容忍度够,可忽略;真要做严格共享内存就得查锁机制。外汇与贵金属复盘工具涉及高杠杆高风险,实盘前请在模拟环境验证这套互斥逻辑。
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; } class="macro">#define def_GlobalVariableReplay "Replay Infos" class="macro">#define def_MaxPosSlider class="num">400 union u_Interprocess { class="type">class="kw">double Value; class="kw">struct st_0 { class="type">bool isPlay; class="type">bool IsUsing; class="type">int iPosShift; }s_Infos; }; class="type">void Init(class="kw">const class="type">bool state = false) { u_Interprocess Info; if (m_szBtnPlay != NULL) class="kw">return; m_id = ChartID(); }