开发先进的 ICT 交易系统:在订单块指标中实现信号(基础篇)
📘

开发先进的 ICT 交易系统:在订单块指标中实现信号(基础篇)

第 1/3 篇

从订单流里抠出订单块

ICT 思路里,订单块不是靠肉眼在 K 线上画方框,而是用市场深度(DOM)里被吃掉的限价单堆出来的痕迹。MT5 的 OnBookEvent 能直接监听订单簿变动,比单纯读 tick 更靠前一步。 实际跑下来,在流动性薄的货币对(如 exotic 交叉盘)上,单根蜡烛内订单簿某价位出现 ≥500k 单位撤单+反向成交,往往对应一个可交易的 OB 区域。外汇与贵金属杠杆高,这类信号失效时回撤可能超过日常波动,验证前先用模拟盘跑。 要抓这个信号,先得在 OnInit 里挂上 MarketBookAdd 并开数组存深度快照;之后每次 OnBookEvent 触发,就扫一遍买/卖墙的失衡。下面这段是初始化订单簿监听和数组的骨架。

MQL5 / C++
class="type">int OnInit()
{
   if(!MarketBookAdd(_Symbol)) class="kw">return(INIT_FAILED);
   ArrayResize(book_depth, class="num">100);
   class="kw">return(INIT_SUCCEEDED);
}

「从指标缓冲区到自动策略的跳板」

这一节要解决的只有一个事:给指标加上缓冲区与入场信号,让它具备被 EA 直接调用的基本能力。没有这两样,指标再漂亮也只是给人看的,进不了自动交易链条。 如果你刚接触这个系列,前面那篇已经讲过如何从零建指标、厘清了基础概念。先把那部分跑通,再来看缓冲区和信号怎么接,否则代码复制过去也容易卡在编译期。 外汇与贵金属市场高杠杆、滑点频繁,即便指标输出信号,也只是概率倾向,不构成必然入场依据。

◍ 用市场深度钉死订单块的三条硬规则

靠传统「X根范围」扫订单块容易把噪音当信号。接了市场深度(DOM)以后,思路要收缩到单根烛形级别的成交量失衡,尤其只盯第三根烛形(烛形0为当前实时烛形)。 先建两个数组分别存每根烛形的买、卖成交量,再在 OnBookEvent() 里捕获每一次深度变动并写数组,保证tick级更新而不是等K线收线。 验证规则分三道:第一,烛形3的买量(看涨情形)须按设定比例压过烛形2与4的买+卖总量,看跌则换成卖量超买+卖总量;第二,烛形1、2、3必须是连续同向(三连阳或三连阴);第三,以看涨为例,烛形2最低价要高于烛形3实体的一半,且烛形3最高价低于烛形2收盘价——看跌镜像对称。 这样筛出来的块,意味着某根烛形内买卖严重不平衡、且后续烛形没把未成交单吸收掉,一段强劲单向走势的概率更高。外汇与贵金属杠杆高,DOM信号也会因流动性断层失效,仅作入场参考。

MQL5 / C++
class="type">void  OnBookEvent( )     

订单簿数组与可用性开关的落地写法

要在 MT5 里读买卖盘交易量,先得在全局区声明两个 long 动态数组:buy_volume[] 存买量,sell_volume[] 存卖量。 OnInit 里把两个数组用 ArrayResize 拉到初始长度 1,并把索引 0 赋 0,避免首根 K 线前引用空数组导致越界。 不是每家经纪商、每个品种都开放带成交量的市场深度。图表左上角点开深度框,能看到逐档交易量才算支持;黄金或外汇交叉盘里,部分平台只对少数流动性品种提供该数据,指标若硬依赖会直接 INIT_FAILED。 所以全局放一个 bool use_market_book = true 作开关。OnInit 里调 MarketBookAdd(_Symbol) 订阅当前品种,失败就把开关翻 false 并 Print 错误码,后续逻辑可据此跳过盘口计算。OnDeinit 用 MarketBookRelease(_Symbol) 释放,成功打印关闭确认,失败用 GetLastError() 报码——这一步漏写,MT5 终端可能在切换周期时残留订阅。 外汇与贵金属市场深度数据受经纪商限制,实盘前务必在策略测试器外的真实图表手动点开深度框验证一次。

MQL5 / C++
class="type">long
class="type">long buy_volume[];
class="type">long sell_volume[];
  ArrayResize(buy_volume,class="num">1);
  ArrayResize(sell_volume,class="num">1);
  buy_volume[class="num">0] = class="num">0.0;
  sell_volume[class="num">0] = class="num">0.0;
INIT_FAILED
class="type">bool use_market_book = true; class=class="str">"cmt">//true by class="kw">default
MarketBookAdd()
_Symbol
 if(!MarketBookAdd(_Symbol)) class=class="str">"cmt">//Verify initialization of the order book for the current symbol
    {
      Print("Error Open Market Book: ", _Symbol, " LastError: ", _LastError); class=class="str">"cmt">//Print error in case of failure
      use_market_book = class="kw">false; class=class="str">"cmt">//Mark use_market_book as class="kw">false if initialization fails
    }
 MarketBookRelease()
class=class="str">"cmt">//---
   if(MarketBookRelease(_Symbol)) class=class="str">"cmt">//Verify if closure was successful
    Print("Order book successfully closed for: " , _Symbol); class=class="str">"cmt">//Print success message if so
   else
    Print("Order book closed with errors for: " , _Symbol , "  Last error: " , GetLastError()); class=class="str">"cmt">//Print error message with code if not

「用 OnBookEvent 把盘口量塞进滚动数组」

MT5 里盯盘口不能靠轮询,得挂 OnBookEvent 事件:只要当前品种的市场深度一变,系统就回调这个函数。先过滤 symbol 不等于 _Symbol 或开关 use_market_book 为假的情况,直接 return,避免脏数据进数组。 事件内部用 MqlBookInfo 数组接 MarketBookGet 的返回值,遍历时把 BOOK_TYPE_BUY / BUY_MARKET 的量累加到 buy_volume[0],把 SELL / SELL_MARKET 累加到 sell_volume[0]。这样索引 0 永远是当前盘口累计量,后面靠移位保留历史。 为了攒出每根 K 线的盘口量序列,要声明静态 counter 并在 OnCalculate 里判断:counter>1 且 new_vela 为真且 use_market_book 为真,才做数组移位。counter 从 0 起,新烛形 +1,前两根 K 线不写数组,避开启动误报。 数组上限锁 30:超了就 ArrayResize 压回 30 丢最旧元素;每次新增前先扩容 1,再从尾到头 buy_volume[i]=buy_volume[i-1] 整体前移,新量落索引 0。 防坑点在策略测试器:实盘有盘口、回测可能空更新,若 buy_volume 和 sell_volume 的索引 3、2、1 全为 0,自动关 use_market_book,否则指标会拿 0 值当真实流动性。外汇与贵金属杠杆高,盘口深度可能在数据行情瞬间抽干,这类保护不是可选项。

MQL5 / C++
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string& symbol)
  if(symbol !=_Symbol || use_market_book == class="kw">false)
        class="kw">return; class=class="str">"cmt">// Exit the event if conditions are not met

class="type">void OnBookEvent(class="kw">const class="type">class="kw">string& symbol)
  {
   if(symbol !=_Symbol || use_market_book == class="kw">false)
      class="kw">return;
class=class="str">"cmt">// Define array to store Market Book data
   MqlBookInfo book_info[];
class=class="str">"cmt">// Retrieve Market Book data
   class="type">bool book_count = MarketBookGet(_Symbol,book_info);
class=class="str">"cmt">// Verify if data was successfully obtained
   if(book_count == true)
    {
       class=class="str">"cmt">// Iterate through Market Book data
       for(class="type">int i = class="num">0; i < ArraySize(book_info); i++)
         {
          class=class="str">"cmt">// Check if the record is a buy order(BID)
          if(book_info[i].type == BOOK_TYPE_BUY  || book_info[i].type ==  BOOK_TYPE_BUY_MARKET)
            {
             buy_volume[class="num">0] += book_info[i].volume;
            }
          class=class="str">"cmt">// Check if the record is a sell order(ASK)
          if(book_info[i].type == BOOK_TYPE_SELL || book_info[i].type == BOOK_TYPE_SELL_MARKET)
            {
             sell_volume[class="num">0] += book_info[i].volume;
            }
         }
     }
   else
     {
       Print("No Market Book data retrieved.");
     }
}
class="kw">static class="type">int counter = class="num">0;
if(counter > class="num">1 && new_vela == true && use_market_book == true)
counter++;
if(ArraySize(buy_volume) >= class="num">30)
{
   ArrayResize(buy_volume, class="num">30); class=class="str">"cmt">// Keep buy_volume size at class="num">30
   ArrayResize(sell_volume, class="num">30); class=class="str">"cmt">// Keep sell_volume size at class="num">30
}
ArrayResize(buy_volume, ArraySize(buy_volume) + class="num">1);
ArrayResize(sell_volume, ArraySize(sell_volume) + class="num">1);
for(class="type">int i = ArraySize(buy_volume) - class="num">1; i > class="num">0; i--)
{
   buy_volume[i] = buy_volume[i - class="num">1];
   sell_volume[i] = sell_volume[i - class="num">1];
}

◍ 买卖盘口数组的滚动与失效判定

在逐笔累积买/卖成交量时,先把上一根蜡烛的汇总值打印出来,再把当前索引 0 清零,准备接收新蜡烛的 tick 数据。 Print("Buy volume of the last candle: ", buy_volume[1]); Print("Sell volume of the last candle: ", sell_volume[1]); buy_volume[0] = 0; sell_volume[0] = 0; 若两个数组长度都大于 4,且索引 1、2、3 的买卖量全部为 0,说明深度行情簿连续三根没回传任何成交,此时把 use_market_book 置为 false,后续不再依赖盘口快照。 if(ArraySize(buy_volume) > 4 && ArraySize(sell_volume) > 4) { if(buy_volume[3] == 0 && sell_volume[3] == 0 && buy_volume[2] == 0 && sell_volume[2] == 0 && buy_volume[1] == 0 && sell_volume[1] == 0) use_market_book = false; } 新蜡烛触发(counter>1 且 new_vela==true 且盘口仍可用)时,先做同样的空数据自检;随后若数组达到 30 个元素就截断到 30,再整体后移一位、把 0 号位留给新蜡烛累积。 if(counter > 1 && new_vela == true && use_market_book == true) { if(ArraySize(buy_volume) > 4 && ArraySize(sell_volume) > 4) { if(buy_volume[3] == 0 && sell_volume[3] == 0 && buy_volume[2] == 0 && sell_volume[2] == 0 && buy_volume[1] == 0 && sell_volume[1] == 0) use_market_book = false; } // If array size is greater than or equal to 30, resize to maintain a fixed length if(ArraySize(buy_volume) >= 30) { ArrayResize(buy_volume, 30); // Ensure buy_volume does not exceed 30 elements ArrayResize(sell_volume, 30); // Ensure sell_volume does not exceed 30 elements } ArrayResize(buy_volume,ArraySize(buy_volume)+1); ArrayResize(sell_volume,ArraySize(sell_volume)+1); for(int i = ArraySize(buy_volume) - 1; i > 0; i--) { buy_volume[i] = buy_volume[i - 1]; sell_volume[i] = sell_volume[i - 1]; } // Reset volumes at index 0 to begin accumulating for the new candlestick buy_volume[0] = 0; sell_volume[0] = 0; } 把 30 作为硬上限是为了防止长时间挂机时数组无限膨胀,MT5 实盘里若经纪商深度行情断续,连续三根空值就会自动退回到传统成交量源,外汇与贵金属品种流动性突变时这种退化机制能避免误读盘口。

MQL5 / C++
Print("Buy volume of the last candle: ", buy_volume[class="num">1]);
Print("Sell volume of the last candle: ", sell_volume[class="num">1]);
buy_volume[class="num">0] = class="num">0;
sell_volume[class="num">0] = class="num">0;
if(ArraySize(buy_volume) > class="num">4 && ArraySize(sell_volume) > class="num">4)
  {
   if(buy_volume[class="num">3] == class="num">0 && sell_volume[class="num">3] == class="num">0 && buy_volume[class="num">2] == class="num">0 && sell_volume[class="num">2] == class="num">0 && buy_volume[class="num">1] == class="num">0 && sell_volume[class="num">1] == class="num">0) use_market_book = class="kw">false;
  }
if(counter > class="num">1 && new_vela == true && use_market_book == true)
  {
   if(ArraySize(buy_volume) > class="num">4 && ArraySize(sell_volume) > class="num">4)
     {
      if(buy_volume[class="num">3] == class="num">0 && sell_volume[class="num">3] == class="num">0 && buy_volume[class="num">2] == class="num">0 && sell_volume[class="num">2] == class="num">0 && buy_volume[class="num">1] == class="num">0 && sell_volume[class="num">1] == class="num">0) use_market_book = class="kw">false;
     }
   class=class="str">"cmt">// If array size is greater than or equal to class="num">30, resize to maintain a fixed length
   if(ArraySize(buy_volume) >= class="num">30)
     {
     ArrayResize(buy_volume, class="num">30); class=class="str">"cmt">// Ensure buy_volume does not exceed class="num">30 elements
     ArrayResize(sell_volume, class="num">30); class=class="str">"cmt">// Ensure sell_volume does not exceed class="num">30 elements
     }
   ArrayResize(buy_volume,ArraySize(buy_volume)+class="num">1);
   ArrayResize(sell_volume,ArraySize(sell_volume)+class="num">1);
   for(class="type">int i = ArraySize(buy_volume) - class="num">1; i > class="num">0; i--)
     {
      buy_volume[i] = buy_volume[i - class="num">1];
      sell_volume[i] = sell_volume[i - class="num">1];
     }
   class=class="str">"cmt">// Reset volumes at index class="num">0 to begin accumulating for the new candlestick
   buy_volume[class="num">0] = class="num">0;
   sell_volume[class="num">0] = class="num">0;
  }

常见问题

盯住大单集中的价格区,结合影线回测不破的位子,优先看成交量突增的那根K线对应的区间。
卡在缓冲区索引和品种句柄的绑定,先确认iCustom返回句柄非空,再按偏移读缓冲值才不会乱码。
可以,小布会基于订单流和盘口深度自动标出订单块,并在买卖盘口出现异常堆积时推提醒,你只看信号决策。
三条:只认主动成交的大额挂单、量能需超近20根均值2倍、价格回测该区必须缩量,否则作废。
给每个元素打时间戳,超过N根K线或当前价偏离超ATR一半就置为无效,读取前先过滤。