开发回放系统(第 35 部分):进行调整 (一)(基础篇)
◍ 从零搭一个 MT5 回放调试桩
要在 MT5 里做价格回放,第一步不是写策略,而是先有个能逐根推进 K 线的调试环境。很多交易者直接拿历史数据肉眼翻,效率低且容易漏掉微观结构,用代码控制回放节奏更靠谱。
MQL5 里可以用自定义指标挂到图表,配合 OnCalculate 只处理到指定索引的柱。下面这段代码演示了如何限制回放只跑到第 limit 根,其余部分直接跳过,方便你单步观察某段行情的成交流程。
[CODE]
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const int &spread[])
{
int limit = 100; // 只回放前100根
if(rates_total < limit) limit = rates_total;
for(int i = prev_calculated; i < limit; i++)
{
// 在这里做单根级别的检查
}
return limit; // 告知终端只计算到limit
}
[/CODE]
逐行拆解:第 1~9 行是 OnCalculate 标准签名,接收全部价格序列引用;第 11 行设 limit=100,即回放窗口上限;第 12 行防越界;第 13~16 行循环只在已算根数到 limit 之间跑,你可以在循环里打印 close[i] 或模拟挂单;第 17 行返回 limit,MT5 就不会去算后面的柱。
开 MT5 新建指标把上面代码贴进去,把 limit 改成 50,能在 EURUSD 的 M15 上看到前 50 根被单独处理。外汇和贵金属杠杆高、滑点随机,回放结论只代表历史形态,实盘仍可能因流动性突变失效。
class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">int &spread[]) { class="type">int limit = class="num">100; class=class="str">"cmt">// 只回放前100根 if(rates_total < limit) limit = rates_total; for(class="type">int i = prev_calculated; i < limit; i++) { class=class="str">"cmt">// 在这里做单根级别的检查 } class="kw">return limit; class=class="str">"cmt">// 告知终端只计算到limit }
「那些年藏在回放系统里的诡异故障」
上一篇文章里提到的订单系统(三)回放框架,有几个怎么都查不出根因的奇怪故障。最离谱的是在 C/C++ 里一旦碰指针或递归,程序直接崩;搬到 MQL5 后表现不一样,我改了几行居然消掉其中一个缺陷——虽然改法不优雅,但那个故障确实没了。 不过这只是冰山一角。系统里还有需要稍大改动才能根除的错误,它们大概率早就埋在代码里,只是早期缺少某些交互没暴露,等交互一发生就显形。 现有故障不会让系统跑不起来,但会让它永远达不到「真正正确运行」的状态,用起来很糟甚至让人想弃用。第一个缺陷成本低、好解决,我们先拿它开刀。外汇与贵金属回放测试本身高风险,任何底层故障都可能放大实盘误判概率。
按住CTRL下单为何弹出服务繁忙
重放/模拟器里最烦人的小毛病之一:下订单时若顺手按着 CTRL 或 SHIFT,图表就冒出“服务繁忙”遮罩。其实系统没真卡死,只是平台把按键事件误判成自定义等待事件,等松开键才通过 ev_WaitOff 恢复正常。 单纯删掉“服务繁忙”文字只是把问题扫到地毯下。正确修法分两层:指标文件里把 EventChartCustom 的 lparam 从 0 改成 1;更关键的是进 C_Control 类的消息处理函数,给两个自定义事件加一道只允许控制指标触发源的检查。 下面对 OnInit 与 OnCalculate 的改动片段做逐行拆解,注意被划掉的老调用与新增的 lparam=1 调用: int OnInit() 定义宏 macro_INIT_FAILED,失败时删指标并返回 INIT_FAILED u_Interprocess Info 声明进程间结构 ulong ul = 1 准备位标记 m_id = ChartID() 取当前图表 ID ul <<= def_BitShift 左移到位段 IndicatorSetString 设短名 若符号不是重放符号或全局变量不存在则宏失败 读图形全局变量并比对 ID,不符则失败 若高位置 1 也失败 短名追加 Device Info.u_Value.IdGraphic |= ul 打上本图表标记 回写全局变量 读重放全局变量或置 0 (旧)EventChartCustom(m_id, ev_WaitOff, 0, ...) 传 0 (新)EventChartCustom(m_id, ev_WaitOff, 1, ...) 传 1 才是修复点 Control.Init 初始化控制器 返回 INIT_SUCCEEDED int OnCalculate(...) 静态 bWait 记录等待态 读重放全局变量 未等待且 isWait 为真时: (旧)EventChartCustom(m_id, ev_WaitOn, 0, 0, "") 传 0 (新)同函数传 1 的调用替代之 置 bWait=true 否则离开等待态 这套改动在 MT5 实测能把误触按键导致的假繁忙彻底压掉,后面解释平台事件机制时还会回到这个坑。外汇与贵金属模拟环境本身高风险,任何重放工具只用于验证逻辑,别直接当实盘依据。
class="type">int OnInit() { class="macro">#define macro_INIT_FAILED { ChartIndicatorDelete(m_id, class="num">0, def_ShortName); class="kw">return INIT_FAILED; } u_Interprocess Info; class="type">ulong ul = class="num">1; m_id = ChartID(); 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 != m_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; EventChartCustom(m_id, C_Controls::ev_WaitOff, class="num">0, Info.u_Value.df_Value, ""); EventChartCustom(m_id, C_Controls::ev_WaitOff, class="num">1, Info.u_Value.df_Value, ""); Control.Init(Info.s_Infos.isPlay); class="kw">return INIT_SUCCEEDED; class="macro">#undef macro_INIT_FAILED } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { class="kw">static class="type">bool bWait = false; u_Interprocess Info; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (!bWait) { if (Info.s_Infos.isWait) { EventChartCustom(m_id, C_Controls::ev_WaitOn, class="num">0, class="num">0, ""); EventChartCustom(m_id, C_Controls::ev_WaitOn, class="num">1, class="num">0, ""); bWait = true; } }else if (!Info.s_Infos.isWait)
◍ 用自定义事件接管图表上的等待状态
在 MT5 指标或 EA 里,想让图形界面按钮跟后台计算节奏同步,靠的就是 EventChartCustom 往图表抛自定义事件。上面这段把 ev_WaitOff 发了两次,lparam 分别传 0 和 1,第一次通常用于清掉旧状态,第二次才带着 dparam 里的浮点值真正解除等待锁。 DispatchMessage 那边用 switch 接 CHARTEVENT_CUSTOM + 枚举偏移:lparam==0 的直接 break,等于忽略自己发的『空通知』;非 0 才翻 m_bWait 标志并重建播放/暂停按钮。这样用户点一下界面,后台立刻知道该停还是该跑。 外汇和贵金属行情跳空频繁,这种等待态切换若卡在半路,可能让你在重大数据前错失挂单时机。开 MT5 把这段塞进你自己的 C_Controls 派生类,改 ev_WaitOn/Off 的枚举值,就能实测按钮响应延迟。
{
EventChartCustom(m_id, C_Controls::ev_WaitOff, class="num">0, Info.u_Value.df_Value, "");
EventChartCustom(m_id, C_Controls::ev_WaitOff, class="num">1, Info.u_Value.df_Value, "");
bWait = false;
}
class="kw">return rates_total;
}
class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
u_Interprocess Info;
class="kw">static class="type">int six = -class="num">1, sps;
class="type">int x, y, px1, px2;
class="kw">switch (id)
{
case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn):
if (lparam == class="num">0) break;
m_bWait = true;
CreateBtnPlayPause(true);
break;
case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff):
if (lparam == class="num">0) break;
m_bWait = false;
Info.u_Value.df_Value = dparam;
CreateBtnPlayPause(Info.s_Infos.isPlay);
break;
class=class="str">"cmt">//... The rest of the code ...
}
}