开发回放系统 市场模拟(第 19 部分):必要的调整·综合运用
「自定义品种里真正要清零的字段」
做回放/模拟前,最容易被忽略的一步是把自定义品种的若干关键值先归零。我们只关心三样东西:跳价大小(SYMBOL_TRADE_TICK_SIZE)、跳价值(SYMBOL_TRADE_TICK_VALUE)和交易量步数(SYMBOL_VOLUME_STEP)。其它配置项大多与目的无关,不必动。 把这三个值初始化成 0,好处是能让自定义品种和实际交易品种在缺失数据时的状态一致。更实际的是,零值方便你后续写测试脚本去抓「配置没填」的 bug——MetaTrader 5 在这方面有个坑:我试过直接调最小交易量参数,终端直接忽略,根本不生效,所以改用步数来替代最小交易量反而更稳。 初始化完必须做校验。下面代码用了一个宏 macroError,逐个检查那三个值是不是还是 0;只要有一个没被配置文件填上,就打印葡萄牙语报错并中断,否则回放服务后续没法跑。注意:外汇与贵金属杠杆品种波动剧烈,模拟配置错误可能在真仓映射时放大风险,务必在模拟账户先验证。 校验逻辑本身很简单:SymbolInfoDouble 取出来等于 0 就说明没初始化。真正要补的是在配置解析里加两个字段——一个跳价值(触发一次函数生成对应数值),一个步数值(触发内部函数调整),改任何品种的回放配置文件都能加,不影响其它流程。
C_Replay(class="kw">const class="type">class="kw">string szFileConfig) { m_ReplayCount = class="num">0; m_dtPrevLoading = class="num">0; m_Ticks.nTicks = class="num">0; Print("************** Market Replay Service **************"); srand(GetTickCount()); GlobalVariableDel(def_GlobalVariableReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol); CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX); CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX); SymbolSelect(def_SymbolReplay, true); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); m_IdReplay = (SetSymbolReplay(szFileConfig) ? class="num">0 : -class="num">1); } class="type">bool ViewReplay(ENUM_TIMEFRAMES arg1) { class="macro">#define macroError(A) { Print(A); class="kw">return false; } u_Interprocess info; if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0) macroError("Configuração do ativo não esta completa, falta declarar o tamanho do ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0) macroError("Configuração do ativo não esta completa, falta declarar o valor do ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0) macroError("Configuração do ativo não esta completa, falta declarar o volume mínimo."); if (m_IdReplay == -class="num">1) class="kw">return false; if ((m_IdReplay = ChartFirst()) > class="num">0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw();
◍ 回放图表初始化与参数解析的底层写法
这段 MQL5 片段负责在 Market Replay 场景下把回放图表拉起来,并等待指标放行。核心循环用 ChartNext 遍历图表句柄,直到找到目标回放图;ChartOpen 以指定交易品种和周期开图后,立刻套用 Market Replay.tpl 模板并强制重绘。 全局变量在这里充当跨模块信令:先删掉旧的 def_GlobalVariableIdGraphics,再建临时变量并写入浮点句柄值;随后用 while 轮询 GlobalVariableCheck(def_GlobalVariableReplay),每次 Sleep(750) 毫秒,直到指标置位或用户停止标志 _StopFlag 触发才退出。750ms 的轮询间隔意味着信令响应延迟大概率在 1 秒以内。 Configs 函数把外部传入的键值串拆成 PATH / POINTSPERTICK / VALUEPERPOINTS / VOLUMEMINIMAL 四类配置。StringSplit 以 '=' 分割且要求恰好两段,再对键名做 trim 后匹配;命中后用 CustomSymbolSetDouble 把 tick size、tick value 直接写进自定义回放品种的合约属性。 别在实盘品种上复用这套全局变量名。外汇与贵金属回放属高杠杆模拟,参数写错可能让小布生成的复盘信号偏离真实点值。
} class="kw">while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); Print("Aguardando permissão do indicador [Market Replay] para iniciar replay ..."); 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">while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750); class="kw">return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); class="macro">#undef macroError } class="kw">inline class="type">bool Configs(class="kw">const class="type">class="kw">string szInfo) { class="kw">const class="type">class="kw">string szList[] = { "PATH", "POINTSPERTICK", "VALUEPERPOINTS", "VOLUMEMINIMAL" }; class="type">class="kw">string szRet[]; class="type">char cWho; if (StringSplit(szInfo, &class="macro">#x27;=&class="macro">#x27;, szRet) == class="num">2) { StringTrimRight(szRet[class="num">0]); StringTrimLeft(szRet[class="num">1]); for (cWho = class="num">0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[class="num">0]) class="kw">break; class="kw">switch (cWho) { case class="num">0: m_szPath = szRet[class="num">1]; class="kw">return true; case class="num">1: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[class="num">1])); class="kw">return true; case class="num">2: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[class="num">1]));
回放品种的量价参数回填
在自定义回放品种的配置解析里,case 3 负责把文本配置中的交易量步长写进模拟品种。它调用 CustomSymbolSetDouble,以 SYMBOL_VOLUME_STEP 为属性标识,把 szRet[1] 经 StringToDouble 转成的双精度值设进去,随后 return true 表示该项生效。 如果 szRet[0] 命中不了任何已定义的 case,会走到 Print 输出“Variable >>…<< undefined.”,提示该配置变量未实现;而当整段 szInfo 连基础格式校验都没过,则打印“Definition of configuratoin >>…<< invalid.”并返回 false,中断本次配置写入。 实盘接这段逻辑时,建议先在脚本里用 Print 把 szRet[0] 和 szRet[1] 打出来核对:EURUSD 回放常设 volume_step 为 0.01,若填成 0.1 会导致后续下单量校验失败。外汇与贵金属模拟回放属高风险验证环境,参数错配只影响回测真实性,不会直接造成实盘亏损,但结论仅具概率意义。
class="kw">return true; case class="num">3: CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[class="num">1])); class="kw">return true; } Print("Variable >>", szRet[class="num">0], "<< undefined."); }else Print("Definition of configuratoin >>", szInfo, "<< invalid."); class="kw">return false; }
「外汇回放还得等下一篇」
这套市场重放系统眼下能跑自定义品种,但外汇数据回放和模拟先别碰。实测里只要切到外汇品种,无论是回放还是模拟模式,都会直接触发数组超界错误,底层还有没处理完的市场机制。 作者明确说配置参数还没做过充分验证,改配置文件时手要稳,错了可能让订单逻辑直接崩。想看进度可以用压缩包里附的自定义品种先跑着,ZIP 体积约 12.9 MB。 外汇这块的坑正在修,下一篇才正式开讲。贵金属和外币对本身高波动高杠杆,等系统能跑外汇回测了,也建议先用小周期自定义数据验证逻辑,再上实盘品种。