开发回放系统 市场模拟(第 22 部分):外汇(III)·综合运用
(3/3)·关闭柱线显示后指标全乱?出价绘图缺陷正拖垮你的外汇回放诊断
用随机价差伪造真实tick流
在回放引擎里,若标志位 bTickReal 为假,系统会走一段合成 tick 的逻辑:用静态变量 BID/ASK 承接上一次报价,再靠 rand() 掷出一个伪随机价差。 价差计算有个隐蔽区间:iRand 落在 29080 到 32767 之间时,才有约一半概率叠加一个 PointsPerTick 的点差,其余情况价差直接为 0。这意味着合成流的买卖差价并非恒定,而是带随机毛刺。 当最新价上穿 ASK 时,ASK 被刷新为 last,BID 退到 last 减价差;下穿 BID 时反之。最后调用 CustomTicksAdd 把这一帧塞进回放品种。外汇与贵金属杠杆高,用这种伪造流做验证时,实盘滑点可能远大于合成环境,结论仅具概率参考。
if (!m_Ticks.bTickReal) { class="kw">static class="type">class="kw">double BID, ASK; class="type">class="kw">double dSpread; class="type">int iRand = rand(); dSpread = PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? PointsPerTick : class="num">0 ) : class="num">0 ); if (tick[class="num">0].last > ASK) { ASK = tick[class="num">0].ask = tick[class="num">0].last; BID = tick[class="num">0].bid = tick[class="num">0].last - dSpread; } if (tick[class="num">0].last < BID) { ASK = tick[class="num">0].ask = tick[class="num">0].last + dSpread; BID = tick[class="num">0].bid = tick[class="num">0].last; } } CustomTicksAdd(def_SymbolReplay, tick); } m_ReplayCount++; class="macro">#undef def_Rate
「把跳价建模从文件类里剥出来」
为了只靠 1 分钟柱线文件就造出可用的跳价流,得先把职责拆干净:C_FileTicks 不再自己算模拟跳价,这部分丢给 C_Simulation。C_Replay 只能看见 C_FileTicks,后者私密继承 C_Simulation,外部永远摸不到模拟逻辑,回放服务读到的要么是真实跳价文件、要么是伪装成真实的模拟数据,隔离做得比较死。 拆分不是语法强制,只是作者不想让单个类突破 1000 行。C_FileTicks 现在用 private 继承拿 C_Simulation 的能力,但受保护数据不外露,只通过一个返回存储跳价数量的函数把结果喂回数组,供后续 C_Replay 调用。 下面这段代码是改造后的核心入口:BarsToTicks 把 CSV 柱线逐根读入,调用 Simulation 生成跳价并塞进 m_Ticks.Info。注意宏 macroRemoveSec 用取模 60 把秒级偏移削掉,def_MaxSizeArray 控制单次数组上限,循环里用 _StopFlag 让终端停止时安全退出。 // 引入柱线与模拟两个头文件 #include "C_FileBars.mqh" #include "C_Simulation.mqh" // 宏:把时间秒数归零(A - A%60) #define macroRemoveSec(A) (A - (A % 60)) // C_FileTicks 私密继承 C_Simulation,内部代码略 class C_FileTicks : private C_Simulation { // ... Internal class code }; // 柱线转跳价函数,传入 CSV 文件名 bool BarsToTicks(const string szFileNameCSV) { C_FileBars *pFileBars; // 柱线文件对象指针 int iMem = m_Ticks.nTicks, // 记录初始跳价数,用于最后比对 iRet; // Simulation 返回的跳价个数 MqlRates rate[1]; // 单根柱线缓冲 MqlTick local[]; // 模拟跳价临时数组 pFileBars = new C_FileBars(szFileNameCSV); // 打开 CSV 柱线文件 ArrayResize(local, def_MaxSizeArray); // 预分配跳价缓冲 Print("Converting bars to ticks. Please wait..."); // 终端打印进度 while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) // 逐根读柱线且未点停止 { ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary); // 扩柱线数组 m_Ticks.Rate[++m_Ticks.nRate] = rate[0]; // 存当前柱线 iRet = Simulation(rate[0], local); // 由 C_Simulation 造跳价,返回数量 for (int c0 = 0; c0 <= iRet; c0++) // 把造出的跳价搬进总缓冲 { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray); // 扩跳价数组 m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; // 存入单跳价 } } ArrayFree(local); // 释放临时数组 delete pFileBars; // 释放文件对象 m_Ticks.bTickReal = false; // 标记当前为模拟跳价 return ((!_StopFlag) && (iMem != m_Ticks.nTicks)); // 未中断且跳价数有变化才返回 true } 在 MT5 里把这段接进自己的回放工程,先跑一个 1 分钟 EURUSD 的 CSV,看 Print 出来的跳价总数是否随 def_MaxSizeArray 变化。外汇与贵金属模拟回放杠杆高、滑点可能失真,实盘前务必用历史数据交叉验证。
class="macro">#include "C_FileBars.mqh" class="macro">#include "C_Simulation.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define macroRemoveSec(A) (A - (A % class="num">60)) class=class="str">"cmt">//+------------------------------------------------------------------+ class C_FileTicks : class="kw">private C_Simulation { class=class="str">"cmt">// ... Internal class code }; class="type">bool BarsToTicks(class="kw">const class="type">class="kw">string szFileNameCSV) { C_FileBars *pFileBars; class="type">int iMem = m_Ticks.nTicks, iRet; class="type">MqlRates rate[class="num">1]; class="type">MqlTick local[]; pFileBars = new C_FileBars(szFileNameCSV); ArrayResize(local, def_MaxSizeArray); Print("Converting bars to ticks. Please wait..."); while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) { ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">3 : def_BarsDiary), def_BarsDiary); m_Ticks.Rate[++m_Ticks.nRate] = rate[class="num">0]; iRet = Simulation(rate[class="num">0], local); for (class="type">int c0 = class="num">0; c0 <= iRet; c0++) { ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + class="num">1), def_MaxSizeArray); m_Ticks.Info[m_Ticks.nTicks++] = local[c0]; } } ArrayFree(local); class="kw">delete pFileBars; m_Ticks.bTickReal = class="kw">false; class="kw">return ((!_StopFlag) && (iMem != m_Ticks.nTicks)); }
◍ 用交易量区分外汇与股市回放模型
在只拿到 1 分钟柱线文件、没有任何外部说明时,系统必须自己判断该用出价(Bid)还是最后成交价(Last)来驱动模拟。用户不该被迫去理解文件里的建模类型,这个适配工作本来就该由程序在后台完成。 区分的关键藏在交易量字段里。对比外汇资产与股市资产的 1 分钟文件片段,除了交易量数值不同,其余结构几乎一致;外汇文件里的交易量普遍偏小且规律稀疏,股市文件则出现明显更大的整手量级。系统读到该值就能判定:偏外汇特征就用出价做交易价,偏股市特征就用最后成交价。 这套判定不依赖文件名或扩展属性,纯粹解析柱线内容。你在 MT5 里随便导出一个 EURUSD 的 1 分钟文件和一个股票 1 分钟文件,用记事本看 Volume 列就能验证差异。外汇/贵金属杠杆交易高风险,回测模型选错会直接扭曲成交逻辑,动手核对一次比看说明更实在。
外汇模拟器得重做绘图逻辑
早先做回放系统时在 1 分钟柱线里借随机游走模拟可能的走势,第 15 部分又专门研究了零成交量或极小跳价成交量的随机游走。但有些市场和资产的含义很特殊,若不把这类情况也覆盖进模拟,服务跑着跑着就会卡死或崩。回放下反而没事,因为只是按已有数据重放,不用去捏造异常走势。 外汇类市场这块最麻烦:之前出价值直接拿去绘图,可要真搞懂系统,有几个关键点和前面看到的完全不同,模拟计算的一部分得重写。附件 Market_Replay_7vc22.zip(约 14.4 MB)里是现行版本,开 MT5 加载后能复现卡死边界。 下一篇再接着拆这部分改动,现在先留个念头:模拟和回放不是一回事,别拿回放稳当就以为模拟也稳。