开发基于订单簿的交易系统(第一部分):指标(基础篇)
📘

开发基于订单簿的交易系统(第一部分):指标(基础篇)

第 1/2 篇

「在 MT5 里把订单簿做成可读指标」

MT5 的订单簿(Market Book)默认只在深度图里可视化,想把它接进 EA 或自定义指标,得靠 MQL5 的 book 相关函数主动抓取。原生接口不会自动推送每一档变动,必须周期性轮询,这对高频逻辑是个隐性成本。 用 MQL5 抓十档深度,核心是先声明 MqlBookInfo 数组,再调 BookGet 把当前买卖挂单填进去。下面这段能直接编译跑,把最优买价和最优卖价打到日志里,方便你确认数据真的在流。 外汇与贵金属杠杆品种里,订单簿只是局部流动性快照,遇数据行情可能瞬间抽空,用它做信号务必当作概率参考而非确定依据。

MQL5 / C++
MqlBookInfo book[];
if(BookGet(book))
{
   class="type">class="kw">double best_bid = book[class="num">0].price;  class=class="str">"cmt">// 第一档买价
   class="type">class="kw">double best_ask = book[ArraySize(book)-class="num">1].price; class=class="str">"cmt">// 末档卖价
   Print("Bid: ", best_bid, " Ask: ", best_ask);
}

◍ 市场深度与订单簿数组的对应关系

市场深度本质是一串待执行的限价单,反映交易者的挂单意图而非真实成交。由于交易者随时可能因行情变化撤单,这些挂单并不必然转化成交易。 在 MQL5 里,SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH) 的返回值精确对应订单簿深度,它代表用于分析的价格水平数组长度的一半——数组前半放限价卖单,后半放限价买单。文档明确,无订单队列的品种该属性返回 0。实测 EURUSD 在流动性时段深度常为 10,即数组总长 20。 深度不一定非要从实时市场深度取。用 SymbolInfoInteger 直接读属性即可,不必依赖 OnBookEvent 或 MarketBookAdd。当然也可以数 OnBookEvent 填充的 MqlBookInfo 元素,结果等价,后文会拆。 比起 MT5 原生订单簿,自绘方案有三个实在好处:图表空间利用率高,直方图尺寸和位置可自定义;簿事件呈现更干净,噪点少;原生不支持回测,后续若做基于磁盘的 BookEvent 存储,策略测试器里也能跑。外汇与贵金属杠杆高、滑点突变频繁,挂单深度只是概率参考,不预示方向。

用脚本克隆出带 .C 后缀的虚拟品种

经纪商不传事件或市场休市时,本地没有实时订单队列缓存,想测深度行情相关指标就只能自己造数据。MQL5 的 CustomBookAdd 函数只认自定义交易品种,所以第一步是把真实品种克隆成一个虚拟资产,再往里灌模拟的 BookEvent。 CloneSymbolTicksAndRates 脚本干的就是这事。它先定义常量:自定义名直接取运行脚本那个品种的 Symbol() 加 “.C”,路径写死在 Forex 下,克隆天数 DAYS_TO_COPY 设为 5,也就是搬源品种最近 5 天的 M1 数据。脚本必须挂在你要克隆的真实品种图表上跑,克隆自定义品种本身没太大意义。 OnStart 里先建一个 CDateTime 对象 timemaster,用 TimeTradeServer 取当前交易时间,DayDec(5) 回退五天,乘 1000 转成毫秒交给 CopyTicks 圈定起点。Bars 函数按这个时间窗算源品种 M1 柱数,随后 CreateCustomSymbol 建品种;若返回码不是 0 也不是 5304(已存在)就直接退出。 CopyTicks 最多重试 3 次,每次失败 Sleep(1000) 再试,确保把分时全部搬过来。跑完后在市场观察列表里会多出 “<原品种>.C”,打开它的新图表,后面接指标和事件生成器即可。若 .C 品种已存在,跳过创建直接开图复用就行。外汇与贵金属属高风险品类,虚拟品种仅用于离线验证,不代表实盘可成交。

MQL5 / C++
class="macro">#define  CUSTOM_SYMBOL_NAME    Symbol()+".C"
class="macro">#define  CUSTOM_SYMBOL_PATH    "Forex"
class="macro">#define  CUSTOM_SYMBOL_ORIGIN  Symbol()
class="macro">#define  DATATICKS_TO_COPY     UINT_MAX
class="macro">#define  DAYS_TO_COPY          class="num">5
class="macro">#include <Tools\DateTime.mqh>
  CDateTime timemaster;
  class="type">class="kw">datetime now = TimeTradeServer();
  timemaster.Date(now);
  timemaster.DayDec(DAYS_TO_COPY);
  class="type">long DaysAgoMsc = class="num">1000 * timemaster.DateTime();
  class="type">int bars_origin = Bars(CUSTOM_SYMBOL_ORIGIN, PERIOD_M1, timemaster.DateTime(), now);
  class="type">int create = CreateCustomSymbol(CUSTOM_SYMBOL_NAME, CUSTOM_SYMBOL_PATH, CUSTOM_SYMBOL_ORIGIN);
  if(create != class="num">0 && create != class="num">5304)
    class="kw">return;
  class="type">MqlTick array[] = {};
  class="type">MqlRates rates[] = {};
  class="type">int attempts = class="num">0;
  while(attempts < class="num">3)
    {
      class="type">int received = CopyTicks(CUSTOM_SYMBOL_ORIGIN, array, COPY_TICKS_ALL, DaysAgoMsc, DATATICKS_TO_COPY);
      if(received != -class="num">1)
        {
          if(GetLastError() == class="num">0)
            class="kw">break;
        }
      attempts++;
      Sleep(class="num">1000);
    }

「用脚本伪造订单簿事件做回测」

依赖订单簿事件的指标在 MT5 回测里有个死穴:自定义品种补不了在线订单簿的分时报价序列。要让这类指标跑通历史测试,只能自己造模拟数据喂进去。 我们没用标准 MathRand(),而是换了一套 32 位随机数实现,主要图它方便生成指定区间整数——脚本里用 RandomInteger(min, max) 直接拿区间值。订单簿深度设成 32,也就是每次迭代吐 64 个价位;嫌多可以把 SYNTH_SYMBOL_MARKET_DEPTH 调小。 算法先查当前品种是不是自定义品种,不是就直接退出。是的话,以日线最后收盘价为垂直锚点:上方 32 档挂卖单(红条),下方 32 档挂买单(浅蓝条),相邻档位价差取 SYMBOL_TRADE_TICK_SIZE。外层跑 20 次迭代,每次暂停在 400~1000 毫秒间随机抽,让分时图看起来更像真盘。外汇和贵金属品种点差跳变剧烈,用模拟簿回测的结论只代表逻辑可达性,实盘订单流可能完全另一回事,属高风险验证。 把下面脚本丢进 MT5 脚本目录,挂到自建自定义品种上跑,就能在「工具—订单簿」里看到生成的 BookEvent 序列,直接拿去测你的指标。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                  GenerateBookEvent.mq5 |
class=class="str">"cmt">//|                                                    Daniel Santos      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Santos"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#define SYNTH_SYMBOL_MARKET_DEPTH        class="num">32
class="macro">#define SYNTH_SYMBOL_BOOK_ITERATIONS     class="num">20
class="macro">#include <Random.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double BidValue, tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
MqlBookInfo    books[];
class="type">int marketDepth = SYNTH_SYMBOL_MARKET_DEPTH;
CRandom rdn;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   if(!SymbolInfoInteger(_Symbol, SYMBOL_CUSTOM)) class=class="str">"cmt">// if the symbol exists
     {
      Print("Custom symbol ", _Symbol, " does not exist");
      class="kw">return;
     }
   else
      BookGenerationLoop();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void BookGenerationLoop()
  {
   class="type">MqlRates BarRates_D1[];
   CopyRates(_Symbol, PERIOD_D1, class="num">0, class="num">1, BarRates_D1);
   if(ArraySize(BarRates_D1) == class="num">0)
      class="kw">return;
   BidValue = BarRates_D1[class="num">0].close;
   ArrayResize(books, class="num">2 * marketDepth);
   for(class="type">int j = class="num">0; j < SYNTH_SYMBOL_BOOK_ITERATIONS; j++)
     {
      for(class="type">int i = class="num">0, j = class="num">0; i < marketDepth; i++)

常见问题

在终端里用脚本把市场深度数组读出来,按买卖挂单量画成直方图或曲线,加载到副图即可直观读盘。
买价挂单量对应 bids 数组、卖价挂单量对应 asks 数组,下标从最优价开始递增,量纲为合约手数。
小布盯盘的 AIGC 会扫描盘口厚度与挂单撤单节奏,对异动直接标注,省去你手动盯深度面板。
真实品种不允许改写盘口,克隆出 .C 虚拟品种后可用脚本注入伪造挂单,从而离线重放订单流。
用脚本随机生成买卖挂单与成交事件写入虚拟品种,跑几轮极端盘口就能暴露代码边界问题。