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

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

第 1/3 篇

◍ 用指标把订单区块画进 MT5

ICT 体系里的订单区块(Order Block)本质是一段价格行为留下的失衡区:在一段急涨或急跌之前,最后一根反向 K 线往往藏着机构建仓痕迹。把这套逻辑固化成指标,交易者就不用每次手动框线,开 MT5 加载后直接在图表上看见候选区块。 本篇定位是「基础到中级」:先用纯价格行为识别(基础级),再叠加指标过滤(中级)。原文给出的系列大纲显示,整份材料分三大块——概述、指标开发、结论,其中开发部分又拆成输入参数、结构函数、检测逻辑、可视化、提醒移除五个子步骤。 从已公开的目录看,该系列在 2025 年 9 月 9 日发布,截至统计有 1 372 次浏览、18 条评论,说明订单区块的量化实现是实盘群体真在盯的需求。你可以先想清楚自己要的是「裸 K 版」还是「指标辅助版」,这决定了后面参数怎么配。

「从聪明钱挂单区到 MT5 代码结构」

订单区块(Order Block)本质是图表上机构因流动性不足而拆单挂出的未成交区域,表现为供需严重失衡。用价格行为识别时,最基础的逻辑是找连续 4 根同向烛形:以前一根收盘低于涨势起点、第一根收盘高于前根开盘为启动,后续三根收盘逐级抬高;区块范围覆盖前一根与第一根看涨烛形。看跌反之。 进阶一层会引入成交量:机构往往先低量吸筹,随后 2~3 根成交量激增确认订单执行。另一种情况是第一根烛形即出现最大成交量峰,且超过前后根,配合第二根主体 60% 穿越第一根极值、第三根延续势头来锁定区块。 落到 MT5 实做,先建自定义指标骨架,用占位绘图消掉 '没有为 indicator00 定义指标图' 的警告。输入参数分组管理: bullish/bearish 颜色、矩形边框宽度与背景层、以及 Order Block search range(往回搜多少根)。该范围若小于 40,OnInit 会报 INIT_PARAMETERS_INCORRECT 并停止——这是实盘调试第一个易踩的坑。 核心结构用一个 OrderBlock 存 time1/price1/price2/name/mitigated,配合动态数组装多个区块;ATR(14) 句柄在初始化时建好用于波动过滤。新K线判定靠 last_sail_time 与 iTime(0) 比对,避免每次报价重算浪费资源。数组用 ArraySetAsSeries 把索引 0 设为最新,CopyOpen/CopyClose/CopyTickVolume/CopyBuffer 拉完数据再跑检测函数。

检测与绘制订单区块的循环骨架

订单区块(OB)指标的核心是一个从最近 K 线向后回溯的 for 循环。原文设定循环从当前烛形最大值开始、到索引 6 截止:当 i > 5 时每次减 1,相当于只扫描最近约 500 根内的形态(由 Rango_universal_busqueda = 500 控制),避免无边界遍历拖慢 MT5。 循环里先把 OHLC 归一化:烛形主体 = 收盘价 - 开盘价,并取分时报价值用于峰值判定;再引入 ATR(14) 作为方向动能过滤,只有 ATR 扩张段才视为有效强劲走势,这样可以压住中段和末端误检。 找到 one_candle 之前的看涨启动烛形后,把价格与时间写进 OrderBlock 结构,mitigated 初始化为 false,再 push 进 ob_alcistas / ob_bajistas 动态数组。绘图不用每次重画,而是用 ObjectMove 挪已有矩形对象,降低终端资源占用。 缓解检查靠结构体里的布尔量 mitigated 和 name 双字段:name 充当去重过滤器,OB 一旦被触碰缓解就永久置 true,若 name 已进缓解字符串数组则不再弹 alert,避免刷屏。外汇与贵金属波动剧烈,OB 被扫后反转概率高但也常假破,实盘前务必在 MT5 用历史数据验证 ATR 阈值。

MQL5 / C++
class="macro">#class="kw">property  indicator_buffers class="num">1
class="macro">#class="kw">property  indicator_plots class="num">1
sinput group "--- Order Block Indicator settings ---"
input        class="type">int   Rango_universal_busqueda = class="num">500; class=class="str">"cmt">// Universal range for searching order blocks
input        class="type">int   Witdth_order_block = class="num">1;         class=class="str">"cmt">// Width of the order block lines
input        class="type">bool  Back_order_block = true; class=class="str">"cmt">// Enable object to be drawn in the background
input        class="type">bool  Fill_order_block = true; class=class="str">"cmt">// Enable fill for the order block rectangle
input        class="type">color Color_Order_Block_Bajista = clrRed;   class=class="str">"cmt">// Assign red class="type">color for bearish order block
input        class="type">color Color_Order_Block_Alcista = clrGreen; class=class="str">"cmt">// Assign green class="type">color for bullish order block
class="type">class="kw">datetime tiempo_ultima_vela;
class="type">int atr_i;
class="kw">struct OrderBlock
{
   class="type">class="kw">datetime time1;      class=class="str">"cmt">// Time of the candle prior to the first candle of the order block
   class="type">class="kw">double price1;       class=class="str">"cmt">// Upper price level of the order block(level class="num">1)
   class="type">class="kw">double price2;       class=class="str">"cmt">// Lower price level of the order block(level class="num">2)
   class="type">class="kw">string name;         class=class="str">"cmt">// Name of the order block
   class="type">bool mitigated;      class=class="str">"cmt">// Status of the order block(true if mitigated, class="kw">false if not)
};
OrderBlocks ob_bajistas[];
OrderBlocks ob_alcistas[];
tiempo_ultima_vela = class="num">0;
atr_i = iATR(_Symbol, PERIOD_CURRENT, class="num">14);

◍ 初始化与每根新K线的触发骨架

EA 启动时先卡两道关口:搜索范围参数 Rango_universal_busqueda 若小于 40,直接 Print 报错并返回 INIT_PARAMETERS_INCORRECT,意味着后续逻辑根本不会跑;ATR 句柄用 iATR(_Symbol, PERIOD_CURRENT, 14) 取,若等于 INVALID_HANDLE 则返回 INIT_FAILED。这两步能挡掉大部分「指标没数据却硬算」的野bug。 OnInit 里把 ob_bajistas、ob_alcistas 两个动态数组先 ArrayResize 成 0,相当于清空上一轮残留的牛熊对象锚点;正常走完返回 INIT_SUCCEEDED。OnDeinit 则调 Eliminar_Objetos() 清图形,再 ArrayFree 释放两数组,避免 MT5 终端里对象越堆越多。 新K线检测靠一个 bool new_vela 和变量 tiempo_ultima_vela。每次用 iTime(_Symbol, PERIOD_CURRENT, 0) 取当前0号K线时间,若与存储值不等就置 new_vela=true 并刷新时间。这样主逻辑包在 if(new_vela==true) 内,只在「新蜡烛开盘」那一刻执行一次,省掉每 tick 重算的浪费。 价格与波动率数据用七个小写数组接:openArray/closeArray/highArray/lowArray 存 OHLC,Time 存时间,atr 存波动率,Volumen 存 tick 量。全部 ArraySetAsSeries(..., true) 改成时间序列视角(索引0=最新K),随后 CopyOpen(_Symbol, PERIOD_CURRENT, 0, Rango_universal_busqueda*2, openArray) 一次性拷贝双倍搜索范围的 Open——比如搜索范围设为 50,就拉 100 根K线的开盘价进数组。外汇与贵金属波动剧烈,参数过小容易漏掉关键结构,建议开 MT5 把 Rango_universal_busqueda 从 40 往上试。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- indicator buffers mapping
   tiempo_ultima_vela = class="num">0;
   atr_i = iATR(_Symbol, PERIOD_CURRENT, class="num">14);
   if(Rango_universal_busqueda < class="num">40)
     {
       Print("Search range too small");
       class="kw">return (INIT_PARAMETERS_INCORRECT);
     }
   if(atr_i == INVALID_HANDLE)
     {
       Print("Error copying data of indicators");
       class="kw">return(INIT_FAILED);
     }
   ArrayResize(ob_bajistas, class="num">0);
   ArrayResize(ob_alcistas, class="num">0);
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  Eliminar_Objetos();
  ArrayFree(ob_bajistas);
  ArrayFree(ob_alcistas);
  }
class="type">bool new_vela = class="kw">false;
if(tiempo_ultima_vela != iTime(_Symbol, PERIOD_CURRENT, class="num">0))
{
    new_vela = true;
    tiempo_ultima_vela = iTime(_Symbol, PERIOD_CURRENT, class="num">0);
}
if(new_vela == true)
{
   class=class="str">"cmt">// Here we will place the main code of the indicator that will run only when a new candlestick opens
}
class="type">class="kw">double openArray[];
class="type">class="kw">double closeArray[];
class="type">class="kw">double highArray[];
class="type">class="kw">double lowArray[];
class="type">class="kw">datetime Time[];
class="type">class="kw">double atr[];
class="type">long Volumen[];
ArraySetAsSeries(openArray, true);
ArraySetAsSeries(closeArray, true);
ArraySetAsSeries(highArray, true);
ArraySetAsSeries(lowArray, true);
ArraySetAsSeries(Time, true);
ArraySetAsSeries(Volumen, true);
ArraySetAsSeries(atr, true);
class="type">int copiedBars = CopyOpen(_Symbol, PERIOD_CURRENT, class="num">0, (Rango_universal_busqueda * class="num">2), openArray);

「订单块数组的去重与追加逻辑」

抓取完 OHLC、成交量与 ATR 缓冲后,下一步是把识别出的订单块写进运行时数组。这里核心不是『见块就存』,而是先查重再扩容,避免同一根 K 线在多次刷新中被重复登记。 多头侧用 AddIndexToArray_alcistas() 处理:先调用 IsDuplicateOrderBlock_alcista() 比对 time1 与 name 字段,若数组中已存在完全一致的记录就直接跳过;否则用 ArraySize() 取当前长度,ArrayResize() 加 1,再把新块挂到末位。 空头侧是完全对称的一套:AddIndexToArray_bajistas() 与 IsDuplicateOrderBlock_bajista() 逻辑一致,只是操作 ob_bajistas 数组。实盘里这种去重机制能显著降低面板重绘时的资源占用——在 Rango_universal_busqueda 设为 50 时,未去重版本可能在一根蜡烛上重复 push 3~4 次同块。 开 MT5 把下面代码丢进 EA 的包含文件,改 ob_alcistas / ob_bajistas 为你自己的结构体数组名,就能直接复用这套增删守门逻辑。

MQL5 / C++
if(copiedBars < class="num">0)
{
   Print("Error copying data from Open: ", GetLastError());
}
CopyClose(_Symbol, PERIOD_CURRENT, class="num">0, (Rango_universal_busqueda * class="num">2), closeArray);
CopyHigh(_Symbol, PERIOD_CURRENT, class="num">0, (Rango_universal_busqueda * class="num">2), highArray);
CopyLow(_Symbol, PERIOD_CURRENT, class="num">0, (Rango_universal_busqueda * class="num">2), lowArray);
CopyTime(_Symbol, PERIOD_CURRENT, class="num">0, (Rango_universal_busqueda * class="num">2), Time);
CopyTickVolume(_Symbol, PERIOD_CURRENT, class="num">0, (Rango_universal_busqueda * class="num">2), Volumen);
CopyBuffer(atr_i, class="num">0, class="num">0, (Rango_universal_busqueda * class="num">2), atr);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|     Functions to Manage and Add Values to the Arrays            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void AddIndexToArray_alcistas(OrderBlocks &newVela_Order_block_alcista)
  {
   if(!IsDuplicateOrderBlock_alcista(newVela_Order_block_alcista))  class=class="str">"cmt">// Here we check if the structure we are about to add already exists in the array
     {
      class="type">int num_orderblocks_alcista = ArraySize(ob_alcistas);  class=class="str">"cmt">// We assign the variable "num_orderblocks_alcista" the current size of the ob_alcistas array
      ArrayResize(ob_alcistas, num_orderblocks_alcista + class="num">1); class=class="str">"cmt">// Resize the array by increasing its size by class="num">1 to make space for a new order block
      ob_alcistas[num_orderblocks_alcista] = newVela_Order_block_alcista; class=class="str">"cmt">// Assign the new order block to the new index(last position) in the array
     }
  }
class="type">bool IsDuplicateOrderBlock_alcista(const OrderBlocks &newBlock)
  {
   for(class="type">int i = class="num">0; i < ArraySize(ob_alcistas); i++) class=class="str">"cmt">//Start a loop to go through all positions of the ob_alcistas array
     {
      if(ob_alcistas[i].time1 == newBlock.time1 &&
          ob_alcistas[i].name == newBlock.name
          ) class=class="str">"cmt">// Check if both time1 and name of the order block already exist in the array
        {
         class="kw">return true; class=class="str">"cmt">// If they do, class="kw">return true (i.e., it is a duplicate)
         break; class=class="str">"cmt">// Exit the loop
        }
     }
   class="kw">return class="kw">false; class=class="str">"cmt">// If no duplicate is found, class="kw">return class="kw">false
  }
class=class="str">"cmt">// This would be the same logic but for bearish order blocks
class="type">void AddIndexToArray_bajistas(OrderBlocks &newVela_Order_block_bajista)
  {
   if(!IsDuplicateOrderBlock_bajista(newVela_Order_block_bajista))
     {
      class="type">int num_orderblocks_bajistas = ArraySize(ob_bajistas);
      ArrayResize(ob_bajistas, num_orderblocks_bajistas + class="num">1);
      ob_bajistas[num_orderblocks_bajistas] = newVela_Order_block_bajista;
     }
  }
class="type">bool IsDuplicateOrderBlock_bajista(const OrderBlocks &newBlock)
  {
   for(class="type">int i = class="num">0; i < ArraySize(ob_bajistas); i++)
     {
      if(ob_bajistas[i].time1 == newBlock.time1 &&
          ob_bajistas[i].name == newBlock.name
          )
        {

订单区块被触碰时间的回扫函数

订单区块(Order Block)画出来之后,真正有价值的是它被后续价格「回踩触碰」的那根 K 线发生在什么时刻。下面两个函数就是干这个的:给定区块价格与起止时间,从过去往现在倒着扫 K 线,命中就返回那根 candle 的时间。 多头区块用 mitigados_alcsitas(),逻辑是只要回扫过程中某根 K 线的 low / open / close / high 任一低于给定 price,就认定发生 mitigation,直接 return Time[i]。注意循环从 startIndex-2 跑到 endIndex+1,也就是刻意跳过区块自身和最新一根,避免自指。 空头版本 mitigado_bajista() 结构完全一致,只是判断方向反过来——价格高于区块才算触碰。两个函数都没找到回踩就 return 0,调用方拿 0 去判断「至今未触碰」即可。 在 MT5 里接这套函数时,建议先把 openArray / closeArray 用 CopyOpen / CopyClose 按 PERIOD_CURRENT 拉满,再传引用进函数;外汇与贵金属杠杆高、滑点跳空频繁,回踩判定仅作概率参考,实盘须自行验证。

MQL5 / C++
class="type">class="kw">datetime  mitigados_alcsitas(class="type">class="kw">double price, class="type">class="kw">double &openArray[], class="type">class="kw">double &closeArray[], class="type">class="kw">double &highArray[], class="type">class="kw">double &lowArray[], class="type">class="kw">datetime &Time[], class="type">class="kw">datetime start, class="type">class="kw">datetime end)
  {
  class="type">int startIndex = iBarShift(_Symbol,PERIOD_CURRENT,start); class=class="str">"cmt">// Using iBarShift we find the index of the candle by passing the "start" time
  class="type">int endIndex = iBarShift(_Symbol, PERIOD_CURRENT, end);   class=class="str">"cmt">// Using iBarShift we find the index of the candle by passing the "end" time
  NormalizeDouble(price,_Digits); class=class="str">"cmt">// Normalize the price we will work with
  for(class="type">int i = startIndex - class="num">2 ; i >= endIndex + class="num">1 ; i--) class=class="str">"cmt">// Start a loop from start(time1 of the order block) to end(time[class="num">1])
    {
    class=class="str">"cmt">//terminated by endIndex which will be time[class="num">0] + class="num">1 = time[class="num">1] --> We are searching for mitigation from past to present(backward)
    NormalizeDouble(lowArray[i],_Digits);
    NormalizeDouble(openArray[i],_Digits);
    NormalizeDouble(highArray[i],_Digits);
    NormalizeDouble(openArray[i],_Digits);
    class=class="str">"cmt">//Normalizamos todas laas variable
    if(price > lowArray[i] || price > openArray[i] || price > closeArray[i] || price > highArray[i]) class=class="str">"cmt">// Check if OHLC closed below price
      {
      class="kw">return Time[i]; class=class="str">"cmt">//If mitigation is found, class="kw">return the time of the candle where it happened
      Print("el orderblock tuvo mitigaciones", TimeToString(end));
      }
    }
  class="kw">return class="num">0; class=class="str">"cmt">//If no mitigation was found, class="kw">return class="num">0
  }
class=class="str">"cmt">// the same in the bearish case but changing something
class=class="str">"cmt">// instead of the price closing below the price
class="type">class="kw">datetime  mitigado_bajista(class="type">class="kw">double price, class="type">class="kw">double &openArray[], class="type">class="kw">double &closeArray[], class="type">class="kw">double &highArray[], class="type">class="kw">double &lowArray[], class="type">class="kw">datetime &Time[], class="type">class="kw">datetime start, class="type">class="kw">datetime end)
  {
  class="type">int startIndex = iBarShift(_Symbol,PERIOD_CURRENT,start);
  class="type">int endIndex = iBarShift(_Symbol, PERIOD_CURRENT, end);
  NormalizeDouble(price,_Digits);
  for(class="type">int i = startIndex - class="num">2 ; i >= endIndex + class="num">1 ; i--)
    {
    NormalizeDouble(lowArray[i],_Digits);
    NormalizeDouble(openArray[i],_Digits);

常见问题

用自定义指标在每根新K线触发时,检测看涨/看跌实体蜡烛,将其高低范围写入数组并调用矩形对象绘制,即可在图表上标出订单区块。
每tick重算会拖慢终端且产生重复块;用新K线触发骨架只在柱闭合后处理一次,能稳定去重并降低资源占用。
可以。把品种页打开,小布会用AIGC比对指标标记的区块与盘口挂单异动,直接告诉你哪些块被触碰、哪些值得关注。
在追加前遍历数组,按时间值和价格区间做去重判断,若已存在相近记录则跳过,只保留首次出现的区块。
从区块生成柱开始向后循环,若后续K线收盘价或下影线进入区块范围,记录该柱时间并返回,用于后续失效或提醒逻辑。