开发基于订单簿的交易系统(第一部分):指标(基础篇)
「在 MT5 里把订单簿做成可读指标」
MT5 的订单簿(Market Book)默认只在深度图里可视化,想把它接进 EA 或自定义指标,得靠 MQL5 的 book 相关函数主动抓取。原生接口不会自动推送每一档变动,必须周期性轮询,这对高频逻辑是个隐性成本。 用 MQL5 抓十档深度,核心是先声明 MqlBookInfo 数组,再调 BookGet 把当前买卖挂单填进去。下面这段能直接编译跑,把最优买价和最优卖价打到日志里,方便你确认数据真的在流。 外汇与贵金属杠杆品种里,订单簿只是局部流动性快照,遇数据行情可能瞬间抽空,用它做信号务必当作概率参考而非确定依据。
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 品种已存在,跳过创建直接开图复用就行。外汇与贵金属属高风险品类,虚拟品种仅用于离线验证,不代表实盘可成交。
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 序列,直接拿去测你的指标。
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++)