开发基于订单簿的交易系统(第一部分):指标·进阶篇
「用随机深度伪造盘口挂单」
这段逻辑在 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 就能直接看到自定义盘口在『市场报价—深度』窗口里的形态。
{
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 成员入手最快。
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 触发,盯盘时挂这两个事件比轮询要省事。外汇与贵金属市场流动性跳变频繁,这类深度直方图只反映当时盘口,不代表后续方向,高杠杆下误读可能放大亏损。
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 数真实数组大小。
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 的实际值打印出来对照,别直接拿去当信号。
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(); }