一步步学习如何利用公允价值缺口(FVG)或市场不平衡性来交易的策略:一种聪明资金的交易方法·进阶篇
📘

一步步学习如何利用公允价值缺口(FVG)或市场不平衡性来交易的策略:一种聪明资金的交易方法·进阶篇

第 2/3 篇

FVG 缺口判定与矩形标记的实现

在 MT5 的 K 线序列里,FVG(公允价格缺口)靠三根连续棒线识别:取第 i 根低点 low0、第 i+2 根高点 high2,若 low0 大于 high2 且两者价差换算成点数后超过 minPts,就倾向构成向上缺口;反之 low2 大于 high0 则可能是向下缺口。 代码里用 iLow / iHigh 取对应价位,再用 NormalizeDouble 除以 _Point 得到以最小报价单位计的差值 gap_L0_H2 与 gap_H0_L2,这一步直接决定你画面上的缺口阈值灵敏度,外汇与贵金属波动属性不同,minPts 不能照抄别人参数。 判定成立后,以中间棒 i+1 的时间 time1 为起点,按 PeriodSeconds(_Period)*FVG_Rec_Ext_Bars 向右延展出 time2,调用 CreateRec 画 OBJ_RECTANGLE。该函数先 ObjectFind 防重名,再写时间/价格锚点与填充色,最后 ChartRedraw 刷新。 每次新建矩形都会把名字塞进 totalFVGs 数组、把中间棒索引存进 barINDICES,两个数组同步 ArrayResize +1。实操时开 MT5 把这段拼进 EA,调 minPts 从 20 点到 50 点观察 XAUUSD 的 15 分钟图,缺口标记密度会明显变化,高风险品种请先开模拟盘验证。

MQL5 / C++
ArrayResize(signalFVGs,class="num">0);
}
ObjectsDeleteAll(class="num">0,FVG_Prefix);
  class="type">class="kw">double low0 = iLow(_Symbol,_Period,i);
  class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2);
  class="type">class="kw">double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits);
  class="type">class="kw">double high0 = iHigh(_Symbol,_Period,i);
  class="type">class="kw">double low2 = iLow(_Symbol,_Period,i+class="num">2);
  class="type">class="kw">double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits);
  class="type">bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts;
  class="type">bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts;
    class="type">class="kw">datetime time1 = iTime(_Symbol,_Period,i+class="num">1);
    class="type">class="kw">double price1 = FVG_UP ? high2 : high0;
    class="type">class="kw">datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars;
    class="type">class="kw">double price2 = FVG_UP ? low0 : low2;
    class="type">class="kw">string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";
    class="type">color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN;
class="type">void CreateRec(class="type">class="kw">string objName,class="type">class="kw">datetime time1,class="type">class="kw">double price1,
                class="type">class="kw">datetime time2, class="type">class="kw">double price2,class="type">color clr){
  if (ObjectFind(class="num">0,objName) < class="num">0){
    ObjectCreate(class="num">0,objName,OBJ_RECTANGLE,class="num">0,time1,price1,time2,price2);
    ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">0,time1);
    ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">0,price1);
    ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">1,time2);
    ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">1,price2);
    ObjectSetInteger(class="num">0,objName,OBJPROP_COLOR,clr);
    ObjectSetInteger(class="num">0,objName,OBJPROP_FILL,true);
    ObjectSetInteger(class="num">0,objName,OBJPROP_BACK,false);
    ChartRedraw(class="num">0);
  }
}
    CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr);
    ArrayResize(totalFVGs,ArraySize(totalFVGs)+class="num">1);
    ArrayResize(barINDICES,ArraySize(barINDICES)+class="num">1);
    totalFVGs[ArraySize(totalFVGs)-class="num">1] = fvgNAME;
    barINDICES[ArraySize(barINDICES)-class="num">1] = i+class="num">1;

◍ FVG 区域被反向刺穿时的回收逻辑

在 MT5 里跑 FVG(公平价值缺口)标记脚本时,最容易被忽略的是缺口被后续 K 线反向刺穿后的边界重算。上面这段代码从已记录的缺口数组尾部倒序遍历,把每个图形对象的名称、起止时间、高低价和颜色全部读出来,再拿去和近期 K 线比。 内层循环从缺口所在 bar 的前一根开始,往回数 FVG_Rec_Ext_Bars 根。只要向下缺口(CLR_DOWN)被某根 bar 的高点越过、或向上缺口(CLR_UP)被某根 bar 的低点跌破,就判定该缺口在此处被「Cut Off」,调用 UpdateRec 把终止时间和价格改写掉并跳出。 若回数到 k==0 还没触发刺穿条件,脚本会打印 OverFlow Detected 并以当前 bar 时间强制收口,避免数组越界死循环。外汇与贵金属市场跳空频繁,这类边界回收直接决定你看到的 FVG 是不是「还活着」,实盘前建议把 FVG_Rec_Ext_Bars 从默认值调到 50~100 之间观察重绘频率。 UpdateRec 本身只做四件小事:用 ObjectSetInteger / ObjectSetDouble 重写对象的起止时间与双边价格,最后 ChartRedraw(0) 强制重绘。循环结束处 ArrayResize 清零两个全局数组,保证下一轮扫描不残留旧引用。

MQL5 / C++
for (class="type">int i=ArraySize(totalFVGs)-class="num">1; i>=class="num">0; i--){
    class="type">class="kw">string objName = totalFVGs[i];
    class="type">class="kw">string fvgNAME = ObjectGetString(class="num">0,objName,OBJPROP_NAME);
    class="type">int barIndex = barINDICES[i];
    class="type">class="kw">datetime timeSTART = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">0);
    class="type">class="kw">datetime timeEND = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">1);
    class="type">class="kw">double fvgLOW = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">0);
    class="type">class="kw">double fvgHIGH = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">1);
    class="type">color fvgColor = (class="type">color)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_COLOR);
    for (class="type">int k=barIndex-class="num">1; k>=(barIndex-FVG_Rec_Ext_Bars); k--){
        class="type">class="kw">datetime barTime = iTime(_Symbol,_Period,k);
        class="type">class="kw">double barLow = iLow(_Symbol,_Period,k);
        class="type">class="kw">double barHigh = iHigh(_Symbol,_Period,k);
        if (k==class="num">0){
            Print("OverFlow Detected @ fvg ",fvgNAME);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
        }
        if ((fvgColor == CLR_DOWN && barHigh > fvgHIGH) ||
            (fvgColor == CLR_UP && barLow < fvgLOW)
        ){
            Print("Cut Off @ bar no: ",k," of Time: ",barTime);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
        }
    }
}
class="type">void UpdateRec(class="type">class="kw">string objName,class="type">class="kw">datetime time1,class="type">class="kw">double price1,
               class="type">class="kw">datetime time2, class="type">class="kw">double price2){
    if (ObjectFind(class="num">0,objName) >= class="num">0){
        ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">0,time1);
        ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">0,price1);
        ObjectSetInteger(class="num">0,objName,OBJPROP_TIME,class="num">1,time2);
        ObjectSetDouble(class="num">0,objName,OBJPROP_PRICE,class="num">1,price2);
        ChartRedraw(class="num">0);
    }
}
ArrayResize(totalFVGs,class="num">0);
ArrayResize(barINDICES,class="num">0);

「初始化时扫描可见 K 线的 FVG 缺口」

EA 启动时先抓当前图表可见 K 线数:用 ChartGetInteger(0,CHART_VISIBLE_BARS) 拿到数值并打印,这一步决定后面循环扫几根。若图表上没有任何 OBJ_RECTANGLE 类型的 FVG 对象,直接把 totalFVGs、barINDICES、signalFVGs 三个数组 Resize 到 0,避免旧数据干扰;随后 ObjectsDeleteAll 按前缀清掉上次画的矩形。 核心扫描在 for(i=0;i<=visibleBars;i++) 里:取第 i 根低点 low0 与第 i+2 根高点 high2,算出 gap_L0_H2=(low0-high2)/_Point 并 NormalizeDouble 到当前品种精度;反向再算 gap_H0_L2。当 low0>high2 且 gap 超过 minPts 判为上方 FVG,low2>high0 且超阈值为下方 FVG——这两个布尔量就是缺口识别的硬条件。 命中后按 i+1 根时间画矩形:time1 取 i+1 的 K 线时间,price1 上方缺口用 high2、下方用 high0;time2 向后推 PeriodSeconds()*FVG_Rec_Ext_Bars 根周期,price2 对应 low0 或 low2。名字拼成 前缀+(时间),颜色按方向取 CLR_UP/CLR_DOWN,调 CreateRec 落框,并把名字塞进 totalFVGs 数组且每次 ArrayResize +1,日志会打出 Old/New ArraySize 方便你核对内存增长。 把下面代码直接贴进 MT5 的 OnInit,把 minPts、FVG_Rec_Ext_Bars、FVG_Prefix、CLR_UP、CLR_DOWN 在外部定义好,挂 EURUSD 15 分钟这类高波动品种,可能扫出 5~20 个缺口矩形;外汇和贵金属杠杆高,实盘前务必先在策略测试器跑历史验证。

MQL5 / C++
class="type">int OnInit(){
   class="type">int visibleBars = (class="type">int)ChartGetInteger(class="num">0,CHART_VISIBLE_BARS);
   Print("Total visible bars on chart = ",visibleBars);
   
   if (ObjectsTotal(class="num">0,class="num">0,OBJ_RECTANGLE)==class="num">0){
      Print("No FVGs Found, Resizing storage arrays to class="num">0 now!!!");
      ArrayResize(totalFVGs,class="num">0);
      ArrayResize(barINDICES,class="num">0);
      ArrayResize(signalFVGs,class="num">0);
   }
   
   ObjectsDeleteAll(class="num">0,FVG_Prefix);
   
   for (class="type">int i=class="num">0; i<=visibleBars; i++){
      class=class="str">"cmt">//Print("Bar Index = ",i);
      class="type">class="kw">double low0 = iLow(_Symbol,_Period,i);
      class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2);
      class="type">class="kw">double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits);
      
      class="type">class="kw">double high0 = iHigh(_Symbol,_Period,i);
      class="type">class="kw">double low2 = iLow(_Symbol,_Period,i+class="num">2);
      class="type">class="kw">double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits);
      
      class="type">bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts;
      class="type">bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts;
      
      if (FVG_UP || FVG_DOWN){
         Print("Bar Index with FVG = ",i+class="num">1);
         class="type">class="kw">datetime time1 = iTime(_Symbol,_Period,i+class="num">1);
         class="type">class="kw">double price1 = FVG_UP ? high2 : high0;
         class="type">class="kw">datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars;
         class="type">class="kw">double price2 = FVG_UP ? low0 : low2;
         class="type">class="kw">string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";
         class="type">color fvgClr = FVG_UP ? CLR_UP : CLR_DOWN;
         CreateRec(fvgNAME,time1,price1,time2,price2,fvgClr);
         Print("Old ArraySize = ",ArraySize(totalFVGs));
         ArrayResize(totalFVGs,ArraySize(totalFVGs)+class="num">1);
         ArrayResize(barINDICES,ArraySize(barINDICES)+class="num">1);
         Print("New ArraySize = ",ArraySize(totalFVGs));
         totalFVGs[ArraySize(totalFVGs)-class="num">1] = fvgNAME;

FVG 缺口被回填的判定与截断逻辑

这段循环干的事很直接:倒序遍历已识别的 FVG 对象,把每个缺口的名称、起止时间、高低价和颜色从图形对象里读出来,再去回看它左侧若干根 K 线,判断缺口是否已被价格回填。 核心判定写在内部 k 循环里:若是下跌缺口(fvgColor == CLR_DOWN),只要某根左侧 K 线的最高价 barHigh 超过了 fvgHIGH,就视为被截断;上涨缺口则看最低价 barLow 是否跌破 fvgLOW。命中即调用 UpdateRec 重写记录并 break,不再往前找。 还有一个边界情况——当 k 递减到 0 仍未触发截断,说明可回看范围溢出,代码会打印 OverFlow Detected 并以当前 bar 时间补全记录。外汇与贵金属市场跳空频繁,这类溢出在极端行情中可能出现概率偏高,实盘前应在 MT5 策略测试器里用真实品种跑一遍看触发频率。 循环结束会把 totalFVGs 和 barINDICES 两个数组 ArrayResize 清 0,函数最终 return INIT_SUCCEEDED。你可以直接把这段贴进 EA 的初始化尾部,把 FVG_Rec_Ext_Bars 调成 20~50 之间,观察日志里 Cut Off 与 OverFlow 的输出分布。

MQL5 / C++
barINDICES[ArraySize(barINDICES)-class="num">1] = i+class="num">1;
   ArrayPrint(totalFVGs);
   ArrayPrint(barINDICES);
   }
   }

   for (class="type">int i=ArraySize(totalFVGs)-class="num">1; i>=class="num">0; i--){
      class="type">class="kw">string objName = totalFVGs[i];
      class="type">class="kw">string fvgNAME = ObjectGetString(class="num">0,objName,OBJPROP_NAME);
      class="type">int barIndex = barINDICES[i];
      class="type">class="kw">datetime timeSTART = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">0);
      class="type">class="kw">datetime timeEND = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_TIME,class="num">1);
      class="type">class="kw">double fvgLOW = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">0);
      class="type">class="kw">double fvgHIGH = ObjectGetDouble(class="num">0,fvgNAME,OBJPROP_PRICE,class="num">1);
      class="type">color fvgColor = (class="type">color)ObjectGetInteger(class="num">0,fvgNAME,OBJPROP_COLOR);
      
      Print("FVG NAME = ",fvgNAME," >No: ",barIndex," TS: ",timeSTART," TE: ",
         timeEND," LOW: ",fvgLOW," HIGH: ",fvgHIGH," CLR = ",fvgColor);
      for (class="type">int k=barIndex-class="num">1; k>=(barIndex-FVG_Rec_Ext_Bars); k--){
         class="type">class="kw">datetime barTime = iTime(_Symbol,_Period,k);
         class="type">class="kw">double barLow = iLow(_Symbol,_Period,k);
         class="type">class="kw">double barHigh = iHigh(_Symbol,_Period,k);
         class=class="str">"cmt">//Print("Bar No: ",k," >Time: ",barTime," >H: ",barHigh," >L: ",barLow);
         
         if (k==class="num">0){
            Print("OverFlow Detected @ fvg ",fvgNAME);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
         }
         
         if ((fvgColor == CLR_DOWN && barHigh > fvgHIGH) ||
            (fvgColor == CLR_UP && barLow < fvgLOW)
         ){
            Print("Cut Off @ bar no: ",k," of Time: ",barTime);
            UpdateRec(fvgNAME,timeSTART,fvgLOW,barTime,fvgHIGH);
            break;
         }
      }
      
   }

   ArrayResize(totalFVGs,class="num">0);
   ArrayResize(barINDICES,class="num">0);
   class="kw">return(INIT_SUCCEEDED);
}
   for (class="type">int i=class="num">0; i<=FVG_Rec_Ext_Bars; i++){class=class="str">"cmt">//... }

◍ FVG 成交缺口的回测与下单触发

这段逻辑先抓前一根低点与前第三根高点,用来界定一个已识别的 FVG(公允价值缺口)边界。low0 取 i+1 根低点,high2 取 i+3 根高点,两者构成缺口的上下沿参照。 随后用 for 循环在 FVG_Rec_Ext_Bars 根内扫描,只要某根 K 线的高或低触碰 fvgLow,就把 fvgExist 置真并跳出。这一步是在确认缺口是否已被后续价格部分回填,属于实盘前必跑的过滤。 真正下单发生在 Bid 上穿 fvgHigh(下行缺口)或 Ask 下破 fvgLow(上行缺口)时。以卖单为例,止损被设为 Ask 加上「缺口高度折成点数再乘 10 个 point」的缓冲,即 SL_sell = Ask + NormalizeDouble((((fvgHigh-fvgLow)/_Point)*10)*_Point, _Digits),这种算法让止损距离随缺口大小浮动,而非固定点数。 下单前连过四道闸:Check1 校验手数、Check2 查保证金、Check3 限交易量、Check4 核交易层级,全过才由 obj_Trade.Sell 或 Buy 发单,并把 signalFVGs[j] 锁真防止重复触发。外汇与贵金属杠杆高,这类自动缺口策略回测胜率可能随品种波动,实盘前务必在 MT5 策略测试器用 0.01 手跑一遍 EURUSD 的 H1 数据。

MQL5 / C++
class="type">class="kw">double low0 = iLow(_Symbol,_Period,i+class="num">1);
class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2+class="num">1);
class="type">bool fvgExist = false;
for (class="type">int k=class="num">1; k<=FVG_Rec_Ext_Bars; k++){
   class="type">class="kw">double barLow = iLow(_Symbol,_Period,k);
   class="type">class="kw">double barHigh = iHigh(_Symbol,_Period,k);
   
   if (barHigh == fvgLow || barLow == fvgLow){
      class=class="str">"cmt">//Print("Found: ",fvgNAME," @ bar ",k);
      fvgExist = true;
      break;
   }
}
class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
if (fvgColor == CLR_DOWN && Bid > fvgHigh && !signalFVGs[j]){
   Print("SELL SIGNAL For(",fvgNAME,") Now @ ",Bid);
   class="type">class="kw">double SL_sell = Ask + NormalizeDouble((((fvgHigh-fvgLow)/_Point)*class="num">10)*_Point,_Digits);
   class="type">class="kw">double trade_lots = Check1_ValidateVolume_Lots(class="num">0.01);
   
   if (Check2_Margin(ORDER_TYPE_SELL,trade_lots) &&
       Check3_VolumeLimit(trade_lots) &&
       Check4_TradeLevels(POSITION_TYPE_SELL,SL_sell,fvgLow)){
      obj_Trade.Sell(trade_lots,_Symbol,Bid,SL_sell,fvgLow);
      signalFVGs[j] = true;
   }
   ArrayPrint(totalFVGs,_Digits," [< >] ");
   ArrayPrint(signalFVGs,_Digits," [< >] ");
}
else if (fvgColor == CLR_UP && Ask < fvgLow && !signalFVGs[j]){
   Print("BUY SIGNAL For(",fvgNAME,") Now @ ",Ask);
   class="type">class="kw">double SL_buy = Bid - NormalizeDouble((((fvgHigh-fvgLow)/_Point)*class="num">10)*_Point,_Digits);
   class="type">class="kw">double trade_lots = Check1_ValidateVolume_Lots(class="num">0.01);
   if (Check2_Margin(ORDER_TYPE_BUY,trade_lots) &&
       Check3_VolumeLimit(trade_lots) &&
       Check4_TradeLevels(POSITION_TYPE_BUY,SL_buy,fvgHigh)){
      obj_Trade.Buy(trade_lots,_Symbol,Ask,SL_buy,fvgHigh);
      signalFVGs[j] = true;

「失效 FVG 的数组清理与 OnTick 实时扫描」

当某根 K 线不再满足 FVG 条件(fvgExist == false),脚本会从 totalFVGs、barTIMEs、signalFVGs 三个同步数组里各移除索引 0 的一条记录。ArrayRemove 返回 bool,只有三个删除都成功才打印剩余数组大小,方便你直接在 MT5 Experts 日志里核对数据有没有错位。 删除成功后用 ArraySize 打印的新长度,应当等于删除前长度减 1;若日志里 FVGs、TIMEs、SIGNALs 三者不一致,说明前面写入逻辑有偏移,这是调试 FVG 指标最常见的坑。 OnTick 里用 for 循环从 i=0 扫到 FVG_Rec_Ext_Bars,拿 i+1 和 i+3 号 K 线算缺口。gap_L0_H2 是 (low0-high2)/_Point 并按 _Digits 规范化,向上 FVG 要求 low0>high2 且 gap 大于 minPts;向下反之。 命中后取 time1 为 i+2 根时间,price1 向上取 high2、向下取 high0,time2 再往后推 FVG_Rec_Ext_Bars 根周期秒数,price2 取对应另一侧价格,最后拼出带前缀和时间的 fvgNAME。外汇与贵金属波动大,FVG 失效概率不低,实盘前务必用策略测试器跑一遍多周期验证。

MQL5 / C++
   }
   ArrayPrint(totalFVGs,_Digits," [< >] ");
   ArrayPrint(signalFVGs,_Digits," [< >] ");
   }
   if (fvgExist == false){
      class="type">bool removeName = ArrayRemove(totalFVGs,class="num">0,class="num">1);
      class="type">bool removeTime = ArrayRemove(barTIMEs,class="num">0,class="num">1);
      class="type">bool removeSignal = ArrayRemove(signalFVGs,class="num">0,class="num">1);
      if (removeName && removeTime && removeSignal){
         Print("Success removing the FVG DATA from the arrays. New Data as Below:");
         Print("FVGs: ",ArraySize(totalFVGs)," TIMEs: ",ArraySize(barTIMEs),
               " SIGNALs: ",ArraySize(signalFVGs));
         ArrayPrint(totalFVGs);
         ArrayPrint(barTIMEs);
         ArrayPrint(signalFVGs);
      }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(){
 
   for (class="type">int i=class="num">0; i<=FVG_Rec_Ext_Bars; i++){
      class="type">class="kw">double low0 = iLow(_Symbol,_Period,i+class="num">1);
      class="type">class="kw">double high2 = iHigh(_Symbol,_Period,i+class="num">2+class="num">1);
      class="type">class="kw">double gap_L0_H2 = NormalizeDouble((low0 - high2)/_Point,_Digits);
      
      class="type">class="kw">double high0 = iHigh(_Symbol,_Period,i+class="num">1);
      class="type">class="kw">double low2 = iLow(_Symbol,_Period,i+class="num">2+class="num">1);
      class="type">class="kw">double gap_H0_L2 = NormalizeDouble((low2 - high0)/_Point,_Digits);
      
      class="type">bool FVG_UP = low0 > high2 && gap_L0_H2 > minPts;
      class="type">bool FVG_DOWN = low2 > high0 && gap_H0_L2 > minPts;
      
      if (FVG_UP || FVG_DOWN){
         class="type">class="kw">datetime time1 = iTime(_Symbol,_Period,i+class="num">1+class="num">1);
         class="type">class="kw">double price1 = FVG_UP ? high2 : high0;
         class="type">class="kw">datetime time2 = time1 + PeriodSeconds(_Period)*FVG_Rec_Ext_Bars;
         class="type">class="kw">double price2 = FVG_UP ? low0 : low2;
         class="type">class="kw">string fvgNAME = FVG_Prefix+"("+TimeToString(time1)+")";

常见问题

看三根连续K线:若第二根实体完全脱离第一和第三根影线范围,中间空白区即为FVG,用矩形标记即可直观显示。
若刺穿未完全回填缺口且收盘价重回缺口外侧,倾向仍有效;只有实体彻底覆盖缺口才判定为回收失效。
可以,小布盯盘的AIGC已内置FVG扫描,打开对应品种页就能看到实时标记的缺口区域和状态。
在启动逻辑里遍历可见K线,按三根K线规则逐组检测,命中即写入数组并画矩形,不必等新Tick。
若回填未过半且未破边界,概率上仍可触发;建议在回测里设截断阈值,实体覆盖超阈值就清理数组不再下单。