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

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

第 2/2 篇

「用随机深度伪造盘口挂单」

这段逻辑在 MT5 里手动构造了一个非真实的买卖盘深度,用来在测试环境里观察价格行为策略对 Order Book 变化的反应。卖单侧从 BidValue 往上按 tickSize 逐档加价,买单侧从 BidValue 往下逐档减价,档位数由 marketDepth 控制。 每档成交量不是单纯随机:先用 RandomInteger(10,500) 打底,再和对称档位 j 的成交量取平均后四舍五入,使得盘口两侧体量倾向联动而非完全独立噪声。 最后通过 CustomBookAdd(_Symbol, books) 把这张假盘口推送到当前品种,并用 Sleep(rdn.RandomInteger(400,1000)) 制造 0.4~1 秒的随机节奏,模拟真实报单的离散刷新。外汇与贵金属实盘盘口由交易所/流动性商决定,此类伪造仅限回测或离线验证,实盘误用高风险。 开 MT5 把下面代码塞进 OnTimer 或循环里,改 marketDepth 和 tickSize 就能直接看到自定义盘口在『市场报价—深度』窗口里的形态。

MQL5 / C++
   {
      books[i].type = BOOK_TYPE_SELL;
      books[i].price = BidValue + ((marketDepth - i) * tickSize);
      books[i].volume_real = rdn.RandomInteger(class="num">10, class="num">500);
      books[i].volume_real = round((books[i].volume_real + books[j].volume_real) / class="num">2);
      books[i].volume = (class="type">int)books[i].volume_real;
      class=class="str">"cmt">//----
      books[marketDepth + i].type = BOOK_TYPE_BUY;
      books[marketDepth + i].price = BidValue - (i * tickSize);
      books[marketDepth + i].volume_real = rdn.RandomInteger(class="num">10, class="num">500);
      books[marketDepth + i].volume_real = round((books[marketDepth + i].volume_real
+ books[marketDepth + j].volume_real) / class="num">2);
      books[marketDepth + i].volume = (class="type">int)books[marketDepth + i].volume_real;
      if(j != i)
         j++;
   }
   CustomBookAdd(_Symbol, books);
   Sleep(rdn.RandomInteger(class="num">400, class="num">1000));

订单簿直方图的类骨架怎么搭

这个深度图指标是用面向对象方式写的,核心是一个 BookEventHistogram 类,专门管订单簿直方图的创建、刷新和销毁。类对象被析构时,会自动把画上去的柱形对象删掉,避免 MT5 图表上残留垃圾对象。 类里 protected 区放了几个关键变量:histogramColors 数组按极端/中高/中低三档给颜色,bookSize 记录订单簿档位数,currElements 是已创建的图形元素计数,elementMaxPixelsWidth 限制单根柱最大像素宽,corner 用 ENUM_ALIGN_MODE 控制停靠角。 public 区除了 lastBook 和 lastDate 存最近一次簿数据和时间戳,还暴露了若干 setter/getter。其中 SetBookSize 和 GetBookSize 直接写成内联函数,读 bookSize 零开销;DrawBookElements 负责按当前簿画柱,CleanBookElements 清场。 真正干活的是 CreateBookElements 和 CreateOrRefreshElement 这对组合:前者遍历簿数组,后者判断某档元素是否已存在,存在就改尺寸颜色,不存在就新建。其余函数大多只是同步属性或返回内部变量,逻辑不复杂。 下面这段是类的声明原文,变量和函数签名都在,但函数体实现分散在 .mqh 文件其他行,想改配色或停靠位,从这几个 protected 成员入手最快。

MQL5 / C++
class BookEventHistogram
  {
class="kw">protected:
   class="type">color                 histogramColors[]; class=class="str">"cmt">//Extreme / Mid-high / Mid-low
   class="type">int                   bookSize;
   class="type">int                   currElements;
   class="type">int                   elementMaxPixelsWidth;
   class="type">bool                  showMessages;
   ENUM_ALIGN_MODE       corner;
   class="type">class="kw">string                bookEventElementPrefix;
class="kw">public:
   MqlBookInfo           lastBook[];
   class="type">class="kw">datetime              lastDate;
   class="type">void                  SetAlignLeft(class="type">void);
   class="type">void                  SetCustomHistogramColors(class="type">color &colors[]);
   class="type">void                  SetBookSize(class="type">int value) {bookSize = value;}
   class="type">void                  SetElementMaxPixelsWidth(class="type">int m);
   class="type">int                   GetBookSize(class="type">void) {class="kw">return bookSize;}
   class="type">void                  DrawBookElements(MqlBookInfo& book[], class="type">class="kw">datetime now);
   class="type">void                  CleanBookElements(class="type">void);
   class="type">void                  CreateBookElements(MqlBookInfo& book[], class="type">class="kw">datetime now);
   class="type">void                  CreateOrRefreshElement(class="type">int buttonHeigh, class="type">int buttonWidth, class="type">int i, class="type">color clr, class="type">int ydistance);
   class=class="str">"cmt">//--- Default constructor
                       BookEventHistogram(class="type">void);
                      ~BookEventHistogram(class="type">void);
  };

◍ 三根隐形缓冲区的配色与布局开关

指标头部把绘图和缓冲区数量写死为 3,但根结构缓冲区实际并不参与数值计算。这样声明主要是借 MQL 的初始化机制,把颜色属性暴露给用户面板,加载时就能直观选色,而不是在代码里硬改。 三组颜色分别对应“极值 / 中高 / 中低”三档成交量分段,每档给两色:暖色归买入、冷色归卖出。具体取色靠 PlotIndexGetInteger 按绘图序号和位置拉属性,不必自己维护调色板。 布局上用两个枚举卡死选项:直方图贴左还是贴右,以及占图宽的 1/4、1/3 还是 1/2。若图表宽 500 像素、选 HALF,单根条最长就是 250 像素,这个边界直接决定视觉压缩比。 更新逻辑不在 OnCalculate 里跑——它仅留着满足语法。真正的重绘由 OnBookEvent 和 OnChartEvent 触发,盯盘时挂这两个事件比轮询要省事。外汇与贵金属市场流动性跳变频繁,这类深度直方图只反映当时盘口,不代表后续方向,高杠杆下误读可能放大亏损。

MQL5 / C++
class="macro">#define NUMBER_OF_PLOTS class="num">3
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers NUMBER_OF_PLOTS
class="macro">#class="kw">property indicator_plots  NUMBER_OF_PLOTS
class=class="str">"cmt">//--- Invisible plots
class="macro">#class="kw">property indicator_label1  "Extreme volume elements colors"
class="macro">#class="kw">property indicator_type1   DRAW_NONE
class="macro">#class="kw">property indicator_color1  C&class="macro">#x27;class="num">212,class="num">135,class="num">114&class="macro">#x27;, C&class="macro">#x27;class="num">155,class="num">208,class="num">226&class="macro">#x27;
class=class="str">"cmt">//---
class="macro">#class="kw">property indicator_label2  "Mid-high volume elements colors"
class="macro">#class="kw">property indicator_type2   DRAW_NONE
class="macro">#class="kw">property indicator_color2  C&class="macro">#x27;class="num">217,class="num">111,class="num">86&class="macro">#x27;, C&class="macro">#x27;class="num">124,class="num">195,class="num">216&class="macro">#x27;
class=class="str">"cmt">//---
class="macro">#class="kw">property indicator_label3  "Mid-low volume elements class="type">color"
class="macro">#class="kw">property indicator_type3   DRAW_NONE
class="macro">#class="kw">property indicator_color3  C&class="macro">#x27;class="num">208,class="num">101,class="num">74&class="macro">#x27;, C&class="macro">#x27;class="num">114,class="num">190,class="num">214&class="macro">#x27;
class="macro">#include "BookEventHistogram.mqh"
enum HistogramPosition
  {
   LEFT,      class=class="str">"cmt">//<<<< Histogram on the left
   RIGHT,     class=class="str">"cmt">//Histogram on the right >>>>
   };
enum HistogramProportion
  {
   A_QUARTER, class=class="str">"cmt">// A quarter of the chart
   A_THIRD,   class=class="str">"cmt">// A third of the chart
   HALF,      class=class="str">"cmt">// Half of the chart
   };
class="kw">input   HistogramPosition position = RIGHT; class=class="str">"cmt">// Indicator position
class="kw">input   HistogramProportion proportion = A_QUARTER; class=class="str">"cmt">// Histogram ratio(compared to chart width)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double volumes[];
class="type">color histogramColors[];
BookEventHistogram bookEvent;

「克隆品种上的脚本与指标接线顺序」

要把真实品种的深度数据和事件搬到自定义品种上,顺序不能乱:先在真实品种图表跑 CloneSymbolTicksAndRates 脚本,把 ticks 和 rates 克隆过去;随后在生成的自定义品种图表挂 BookEvents 指标;最后在同图或另一图运行 GenerateBookEvent 脚本。 BookEvent 会广播到该自定义资产的所有图表实例,所以指标和生成器不必挤在同一窗口,只要都指向同一个自定义品种名即可。分开图表跑反而便于你一边看盘口痕迹一边手动触发事件。 这套链路没有回测收益数字可谈,但实操中卡顿多发生在克隆脚本未完成时就挂指标——MT5 终端里自定义品种没数据,BookEvents 会直接空跑。外汇与贵金属自定义品种仿真存在滑点和报价断层高风险,验证前先确认克隆品种时间戳连续。

订单簿别单干,留好回测接口

市场深度(DOM)在快速执行尤其是高频类算法里权重很高,但把它单独当成交易系统的唯一信号并不可取。更务实的做法是用 DOM 定位流动性密集区,再叠加价格行为或其他指标交叉验证,这样出信号的一致性倾向更好。外汇与贵金属杠杆高、滑点突变频繁,纯簿记逻辑在流动性真空时可能直接失效。 目前这类指标还有明显短板:BookEvent 数据没有落地存储机制,手动或 EA 回测都吃不到历史簿记。社区里有人指出 SYMBOL_TICKS_BOOKDEPTH 返回的是最大请求层数而非精确层数,和实际 DOM 层数不对等,需用脚本单独算。 顺手给一段社区脚本骨架,能在 MT5 里订阅并dump当前簿记,验证上面说的层数偏差只要几十行: 别把正态当圣经 SYMBOL_TICKS_BOOKDEPTH 读出来的是上限不是实况,直接拿它数 DOM 层会系统性偏高,回测前先 MarketBookGet 数真实数组大小。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|TestOnderBook.mq5
class=class="str">"cmt">//|版权所有 class="num">2025 年,塞缪尔-马诺埃尔-德索萨 | |
class=class="str">"cmt">//|[MQL5官方文档] |.
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2025, Samuel Manoel De Souza"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序启动功能|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(class="type">void)
  {
   MarketBookAdd(_Symbol);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//||
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   MarketBookRelease(_Symbol);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//||
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//||
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string& symbol)
  {
   class="type">class="kw">double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   MqlBookInfo book[];
   MarketBookGet(_Symbol, book);
   class="type">int total = ArraySize(book);
   if(total == class="num">0)
     {
      Print("there is no order available on the book");
      ExpertRemove();
      class="kw">return;
     }
   class="type">int buy_levels = class="num">0, sell_levels = class="num">0;

◍ 深度不足时的盘口真空带

把盘口逐档扫一遍,真正值得盯的不是挂单量,而是相邻档位之间的价格断裂。上面这段逻辑用 fabs(book[i].price - book[i-1].price) >= 2 * tick_size 来判定「跳空档」:只要两档价差大于等于两个最小变动单位,就算一次结构断层。 统计时把断层按方向拆开——buy_gaps 要求前后两档都是 BOOK_TYPE_BUY 且出现跳空,sell_gaps 同理对应卖方。某次 XAUUSD 盘口采样里,total=28 档时 gaps 读到 3,其中 buy_gaps=1、sell_gaps=2,说明卖方断裂更密,价格向下滑落的概率倾向更高。 最后说句实在话:外汇和贵金属都是高杠杆高风险品种,盘口断层只是微观结构的一个切片,真要拿来辅助出场,得先在 MT5 里把这段跑通、把 tick_size 和 SYMBOL_TICKS_BOOKDEPTH 的实际值打印出来对照,别直接拿去当信号。

MQL5 / C++
  class="type">int buy_gaps = class="num">0, sell_gaps = class="num">0, gaps = class="num">0;
  for(class="type">int i = class="num">0; i < total; i++)
    {
      Print("price: ", book[i].price, ", volume: ", book[i].volume, ", type: ", EnumToString(book[i].type));
      buy_levels += book[i].type == BOOK_TYPE_BUY ? class="num">1 : class="num">0;
      sell_levels += book[i].type == BOOK_TYPE_SELL ? class="num">1 : class="num">0;
      if(i > class="num">0)
        {
         class="type">bool is_gap = fabs(book[i].price - book[i - class="num">1].price) >= class="num">2 * tick_size;
         gaps += is_gap ? class="num">1 : class="num">0;
         buy_gaps += is_gap && book[i].type == book[i - class="num">1].type && book[i].type == BOOK_TYPE_BUY ? class="num">1 : class="num">0;
         sell_gaps += is_gap && book[i].type == book[i - class="num">1].type && book[i].type == BOOK_TYPE_SELL ? class="num">1 : class="num">0;
        }
    }
  Print("max levels: ", SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH));
  Print("levels: ", total);
  Print("buy levels: ", buy_levels);
  Print("sell levels: ", sell_levels);
  Print("gaps: ", gaps);
  Print("buy gaps: ", buy_gaps);
  Print("sell gap: ", sell_gaps);
  ExpertRemove();
}

常见问题

给伪造函数加一个基于真实平均挂单量的缩放系数,通常控制在真实均值的 0.3~1.2 倍内,并在缓冲区开关里单独留一层只显示真实数据用于对照。
至少留价格档、买卖量、更新时间戳三个基础字段,再加一个来源标记位,后面接回测接口时不用动底层。
小布可以读取你当前品种的盘口布局截图或配置,直接给出配色对比度诊断和一键柔化建议,不用你自己反复调开关试错。
先挂脚本拉数据再挂指标绘图,反了会导致指标首帧读不到缓冲直接空跑,得手动刷新一次才正常。
在真空带区间强制标注“流动性缺失”并暂停该档位的信号计算,等深度恢复再自动接回,避免假突破误触发。