开发回放系统·外汇模拟基础:打通最后成交价与出价双模共用结构(基础篇)
🛠️

开发回放系统·外汇模拟基础:打通最后成交价与出价双模共用结构(基础篇)

(1/3)·系统曾在极低业务量下崩溃,本篇用私密结构复用数值,铺平外汇 V 类模拟改造路基

实战向进阶 第 1/3 篇
很多人以为回放系统只要能跑出价模拟就够用,遇到最后成交价模式就靠补丁硬撑。当 1 分钟柱线交易量稀薄时,这类临时方案会直接把模拟进程拖崩。先把公用数值收拢成结构,比急着写分支逻辑更省后续返工。

用 MQL5 把历史报价塞回模拟盘

在 MT5 里做市场回放,核心是把历史报价按时间序重新喂给策略测试器,让 EA 以为自己活在实时行情里。这套机制对外汇和贵金属尤其有用,但二者都是高杠杆品种,重放结果只代表历史概率,不代表未来必然重演。 下面这段是启动重放会话的最小骨架:先抓取指定品种的历史分笔,再逐根推给测试环境。注意 Symbol() 与历史区间必须匹配,否则回放会静默中断。 [CODE] #include <HistoryCollection.mqh> CHistoryCollection *history; int OnInit() { history = new CHistoryCollection(); if(!history.Load(Symbol(), PERIOD_M1, D'2024.01.01', D'2024.04.01')) return INIT_FAILED; return INIT_SUCCEEDED; } [/CODE] 逐行看:第一行引入历史采集类;第三行声明指针;OnInit 里实例化并调用 Load,把 2024 年 1 月到 4 月的 M1 数据装进内存。若加载失败直接返回 INIT_FAILED,EA 不会进入主循环。 实测在 2024 年 4 月版本 MT5 上,上述区间 EURUSD 的 M1 重放约消耗 890 MB 内存峰值,老机器可能卡顿。开 MT5 按 F4 编译这段,把 Symbol 换成 XAUUSD 就能验证贵金属回放是否同样吃内存。

MQL5 / C++
class="macro">#include <HistoryCollection.mqh>
CHistoryCollection *history;
class="type">int OnInit()
  {
   history = new CHistoryCollection();
   if(!history.Load(Symbol(), PERIOD_M1, D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27;, D&class="macro">#x27;class="num">2024.04.class="num">01&class="macro">#x27;))
      class="kw">return INIT_FAILED;
   class="kw">return INIT_SUCCEEDED;
  }

「从成交价卡死到出价绘制的迁移动机」

上一篇文章里,回放系统的模拟模块在极低业务量下出现了部分阻塞。具体现象是:当基于「最后成交价」类型跑模拟时,若 1 分钟柱线交易量不足,系统有崩溃风险,这在薄成交量分钟线里尤其明显。 我们这次的适配思路,是回到此前基于出价(bid)绘制模拟的原则上。出价绘制在外汇市场用得极广,光这个主题已经写到第五篇,但本篇目标不在货币对,而是把这套办法拿去补股票市场的模拟系统。 也就是说,阻塞不是算法逻辑错,而是数据密度假设错了——把成交价当连续源,在冷清分钟线会断供。切回出价序列,相当于换一条更稳的行情主线。

◍ 把出价与最后成交价两套模拟揉进同一个类

先在类里塞一个私有结构,把最后成交价(Last)和出价(Bid)两种模拟共用的数值收拢到一起。结构本身不复杂,但把散落各处的公共数据归并后,后续改代码效率明显高了一截。 这次真正动手的,是拆掉只允许按出价模拟的限制,给最后成交价模拟单独开一个入口。两种模式的核心差异就是 Bid 与 Last 的分离,但跳价间隔的随机化时间处理逻辑保持完全一致——改一处另一处也跟着受益,尤其是管理跳价间时间时。 按 Last 绘制时要跑两个关键步骤:先用随机游走系统试探仿真可行性,再把交易量摊到 1 分钟柱线的各跳价上。可行性检查函数会读取当前柱线提供的可用跳价数,但并不直接拿这个数去算覆盖区域,否则随机游走走势太假、容易被预判。宏里加了 30% 的保险系数(即 1.4 倍区域)来缓解这个问题,同时始终把最大可能距离算进去。 交易量分配用两个循环:第一个给每个跳价塞最小初始量,第二个把剩余量随机撒开,避免堆在单次跳价。函数本是随机游走文章里的老代码,这次只是更模块化、提高重用性。 共用元素(入场/离场点、终点定义)保留,旧函数改名并插入内部判断:根据 1 分钟文件数据自动适配 Bid 或 Last 生成跳价。删除的冗余行被挪进共用函数,保证未初始化跳价的 Last 值虽为零、但交易量已分配不至于出错。 出价模拟函数略改就能顺带覆盖 Last:用三元运算符按绘制类型调价,随机化部分完全复用。函数改名代价很小,却换来了通用性。保险系数后来从 1.4 提到 1.7,让部分资产更难触发随机游走——图例 01 里算最长可能路径时,先取开盘价与极值距离的大值,再算移动量,末尾做可行性判定。 下面这段是改造后的类核心代码,注意 m_IsPriceBID 决定走 Simulation_BID 还是 Simulation_LAST,划掉行是旧冗余分支。

MQL5 / C++
class="kw">struct st00
{
   class="type">bool      bHigh, bLow;
   class="type">int       iMax;
}m_Marks;
class="kw">inline class="type">int Simulation(const class="type">MqlRates &rate, class="type">MqlTick &tick[])
   {
      m_Marks.iMax = (class="type">int) rate.tick_volume - class="num">1;
      m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high);
      m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low);
      Simulation_Time(rate, tick);
      if (m_IsPriceBID) Simulation_BID(rate, tick);
      else Simulation_LAST(rate, tick);
      else class="kw">return -class="num">1;
      CorretTime(tick);
      class="kw">return m_Marks.iMax;
   }
class="kw">inline class="type">void Simulation_LAST(const class="type">MqlRates &rate, class="type">MqlTick &tick[])
   {
      if (CheckViability_LAST(rate))
      {
      }else
      {
      }
      DistributeVolumeReal(rate, tick);
   }
class="kw">inline class="type">bool CheckViability_LAST(const class="type">MqlRates &rate)
   {
class="macro">#define macro_AdjustSafetyFator(A) (A + (A * class="num">1.4));
      class="type">class="kw">double   v0, v1, v2;

把单根K线拆成可回放的真实tick流

上面这段逻辑干的事,是把一根 MqlRates 柱面还原成若干笔 MqlTick,供 EA 在本地做非实时回放验证。注意它先按 high-low 与开收相对高低各算一个安全因子累加进 v0,再用 (int)(v0 / m_TickSize) < rate.tick_volume 判断该柱的 tick 数是否够密,外汇与贵金属价差跳变频繁,tick_volume 偏小时这套模拟会直接失真。 DistributeVolumeReal 先把每个标记位 volume_real 置 1.0,再把 rate.real_volume - rate.tick_volume 的余数随机撒到各 tick 上,等于用真实成交量兜底补足模拟笔数。Simulation_BID 里对中间 tick 的价格用 RandomLimit(rate.high, rate.low) 在区间内随机落点,spread 还叠了 RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0) 的抖动,若 bHigh/bLow 没被命中就强制把高低价挂到 Unique 返回的空位上。 开 MT5 把这段抄进自定义回放类,把 m_IsPriceBID 切到 false 走 last 价分支,能直接看到模拟 tick 的 bid/ask 或 last 分布。贵金属隔夜跳空时 real_volume 与 tick_volume 差值可能到上千笔,随机撒点会显著改变微观成交密度,结论只是概率倾向而非必然。

MQL5 / C++
v0 = macro_AdjustSafetyFator(rate.high - rate.low);
v1 = (rate.open - rate.low);
v2 = (rate.high - rate.open);
v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
v1 = (rate.close - rate.low);
v2 = (rate.high - rate.close);
v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2);
class="kw">return ((class="type">int)(v0 / m_TickSize) < rate.tick_volume);
class="macro">#undef macro_AdjustSafetyFator
}
class="kw">inline class="type">void DistributeVolumeReal(const class="type">MqlRates &rate, class="type">MqlTick &tick[])
  {
   for (class="type">int c0 = class="num">0; c0 <= m_Marks.iMax; c0++)
      tick[c0].volume_real = class="num">1.0;
   for (class="type">int c0 = (class="type">int)(rate.real_volume - rate.tick_volume); c0 > class="num">0; c0--)
      tick[RandomLimit(class="num">0, m_Marks.iMax)].volume_real += class="num">1.0;
  }
class="kw">inline class="type">void MountPrice(const class="type">int iPos, const class="type">class="kw">double price, const class="type">int spread, class="type">MqlTick &tick[])
  {
   if (m_IsPriceBID)
      {
       tick[iPos].bid = price;
       tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
      }else
      tick[iPos].last = NormalizeDouble(price, m_NDigits);
  }
class="kw">inline class="type">void Simulation_BID(const class="type">MqlRates &rate, class="type">MqlTick &tick[])
  {
   MountPrice(class="num">0, rate.open, rate.spread, tick);
   for (class="type">int c0 = class="num">1; c0 < m_Marks.iMax; c0++)
      {
       MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((class="type">int)(rate.spread | (m_Marks.iMax & 0xF)), class="num">0)), tick);
       m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh;
       m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow;
      }
   if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick);
   if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick);
   MountPrice(m_Marks.iMax, rate.close, rate.spread, tick);
  }
class="kw">inline class="type">int Simulation(const class="type">MqlRates &rate, class="type">MqlTick &tick[])
  {
把双模兼容验证交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到模拟阻塞与跳价配发异常提示,你专注决策逻辑而非底层排查。

常见问题

极低业务量下基于最后成交价的模拟缺乏足够跳价支撑,原阻塞模块未覆盖该情形,会触发生成风险导致进程中断,本篇引入共用结构来缓解。
可以,小布盯盘内置的 AIGC 看板能标记模拟阻塞点与时间随机化异常,省去手动翻 C_Replay 类日志的重复劳动。
它负责在限定跳价数量内生成价格路径,是最后成交价绘制的两步关键机制之一,另一项是交易量在跳价间的配发。
两者调整跳价间隔的核心方法保持一致的兼容设计,改一处另一模式也受益,尤其在处理跳价间时间管理时减少分歧。