逆公允价值缺口(IFVG)交易策略·进阶篇
🔄

逆公允价值缺口(IFVG)交易策略·进阶篇

(2/3)· 当价格无视FVG支撑穿越缺口,传统回补逻辑为何会让你错失反向信号

案例拆解 第 2/3 篇
很多交易者把FVG回补当成趋势延续的保险,价格一碰缺口就等原方向发力。可当价格直接穿过缺口毫无反弹,你若还死守旧逻辑,往往正好站在反转的反面。IFVG要教的,就是读懂这种“无视”背后的机构退场。

挂单逻辑与时段门禁的底层拼装

这段执行层代码把卖单和买单价位都做了标准化处理,避免跨品种小数位不一致导致的下单报错。以卖单为例,先抓当前 BID,再用 NormalizeDouble 按 _Digits 截断;TP 在 bid 下方 tpp*_Point,SL 在 bid 上方 slp*_Point,最后 trade.Sell 带参进场并把订单号写回 sellpos。 买入镜像对称:ask 取 SYMBOL_ASK,tp 向上加、sl 向下减,订单号存 buypos。两个函数外层都包了 IsWithinTradingHours(),意思很直白——不在 startHour~endHour 的服务器交易时段内,单子根本不会发出去。 IsGapValid 给 FVG 缺口设了硬边界:anyGap 必须落在 gapMinPoint 到 gapMaxPoint 个 _Point 之间,且 IsReacted() 返回真才算有效。IsReacted 里从 i=4 开始扫 lookBack 根 K 线,用 count1/count2 计数反应强度,这是后续过滤假缺口的关键闸门。 外汇与贵金属杠杆高、滑点跳空频繁,上述点位计算仅描述代码行为,实盘触发概率与胜率须自行在 MT5 策略测试器验证。

MQL5 / C++
class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
 bid = NormalizeDouble(bid,_Digits);
 class="type">class="kw">double tp = bid - tpp * _Point;
 tp = NormalizeDouble(tp, _Digits);
 class="type">class="kw">double sl = bid + slp * _Point;
 sl = NormalizeDouble(sl, _Digits);
 trade.Sell(lott,_Symbol,bid,sl,tp);
 sellpos = trade.ResultOrder();
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Execute buy trade function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void executeBuy() {
 if (IsWithinTradingHours()){
  class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
  ask = NormalizeDouble(ask,_Digits);
  class="type">class="kw">double tp = ask + tpp * _Point;
  tp = NormalizeDouble(tp, _Digits);
  class="type">class="kw">double sl = ask - slp * _Point;
  sl = NormalizeDouble(sl, _Digits);
  trade.Buy(lott,_Symbol,ask,sl,tp);
  buypos= trade.ResultOrder();
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if is trading hours                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsWithinTradingHours()
{
 class="type">class="kw">datetime currentTime = TimeTradeServer();
 class="type">MqlDateTime timeStruct;
 TimeToStruct(currentTime, timeStruct);
 class="type">int currentHour = timeStruct.hour;
 if (currentHour >= startHour && currentHour < endHour)
    class="kw">return true;
 else
    class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   Function to validate the FVG gap                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsGapValid(){
 if (anyGap<=gapMaxPoint*_Point && anyGap>=gapMinPoint*_Point &&IsReacted()) class="kw">return true;  
 else class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   Check for gap reaction to validate its strength               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsReacted(){
 class="type">int count1 = class="num">0;
 class="type">int count2 = class="num">0;
 for (class="type">int i = class="num">4; i < lookBack; i++){

◍ 缺口回补与突破的判定逻辑

识别 FVG(公允价值缺口)后,真正决定它是否失效要看价格是否把缺口区域回补了两次以上。下面这段循环取每根 K 线的高低点,和已知缺口上下沿 anyGapHigh / anyGapLow 做比对:若高点嵌在缺口内且低点跌破下沿,记一次 count1;反之低点嵌在缺口内且高点刺穿上沿,记一次 count2。当任一计数 ≥2,函数返回 true,意味着该缺口倾向已被市场多次填平。 double aLow = iLow(_Symbol,PERIOD_CURRENT,i); double aHigh = iHigh(_Symbol,PERIOD_CURRENT,i); if (aHigh<anyGapHigh&&aHigh>anyGapLow&&aLow<anyGapLow){ count1++; } else if (aLow<anyGapHigh&&aLow>anyGapLow&&aHigh>anyGapHigh){ count2++; } }

if (count1>=2count2>=2) return true;

else return false; 上方代码逐行拆解:第1行取当前品种当前周期第 i 根 K 线最低价赋给 aLow;第2行取同根 K 线最高价赋给 aHigh;第3–7行判断若最高价在缺口内、最低价跌破缺口下沿则 count1 加一;第8–10行反之则 count2 加一;第12–13行只要任一计数达 2 就返回 true,否则 false。 突破方向的确认要看上一根已收出的 K 线。IsBrokenUp 取 lastClosedIndex=1 的开盘与收盘,要求开盘在 gapHigh 下、收盘站上 gapHigh,且超出幅度小于 maxBreakoutPoints*_Point;同时当前 FVG 状态为 -1(看跌缺口)才返回 true。IsBrokenLow 对称处理:开盘在 gapLow 上、收盘跌破 gapLow 且幅度受限,且 currentFVGstatus==1 时返回 true。 void getFVG() 里只回看最近 3 根内的结构:for 循环 i 从 1 到 2,用 iTime 取当前与往前第 2 根的時間,再取 currentLow = iLow(_Symbol,PERIOD_CURRENT,i) 定位最新缺口下沿。外汇与贵金属波动剧烈,这类判定仅提供概率倾向,实盘请先在 MT5 策略测试器用历史数据验证参数 maxBreakoutPoints 的灵敏度。

MQL5 / C++
class="type">class="kw">double aLow = iLow(_Symbol,PERIOD_CURRENT,i);
class="type">class="kw">double aHigh = iHigh(_Symbol,PERIOD_CURRENT,i);
if   (aHigh<anyGapHigh&&aHigh>anyGapLow&&aLow<anyGapLow){
   count1++;
   
}
else if (aLow<anyGapHigh&&aLow>anyGapLow&&aHigh>anyGapHigh){
   count2++;
}

}
if (count1>=class="num">2||count2>=class="num">2) class="kw">return true;
else class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|      Check if price broke out to the upside of the gap            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsBrokenUp(){
    class="type">int lastClosedIndex = class="num">1;
    class="type">class="kw">double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex);
    class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex);
    if (lastOpen < gapHigh && lastClose > gapHigh&&(lastClose-gapHigh)<maxBreakoutPoints*_Point)
    {
        if(currentFVGstatus==-class="num">1){
            class="kw">return true;}
    }
    class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|      Check if price broke out to the downside of the gap          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsBrokenLow(){
    class="type">int lastClosedIndex = class="num">1;
    class="type">class="kw">double lastOpen = iOpen(_Symbol, PERIOD_CURRENT, lastClosedIndex);
    class="type">class="kw">double lastClose = iClose(_Symbol, PERIOD_CURRENT, lastClosedIndex);
    
    if (lastOpen > gapLow && lastClose < gapLow&&(gapLow -lastClose)<maxBreakoutPoints*_Point)
    {
         if(currentFVGstatus==class="num">1){
            class="kw">return true;}
    }
    class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| To get the most recent Fair Value Gap(FVG)                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void getFVG()
{
    class=class="str">"cmt">// Loop through the bars to find the most recent FVG
    for (class="type">int i = class="num">1; i < class="num">3; i++)
    {
        class="type">class="kw">datetime currentTime = iTime(_Symbol,PERIOD_CURRENT, i);
        class="type">class="kw">datetime previousTime = iTime(_Symbol,PERIOD_CURRENT, i + class="num">2);
        class=class="str">"cmt">// Get the high and low of the current and previous bars
        class="type">class="kw">double currentLow = iLow(_Symbol,PERIOD_CURRENT, i);

「用三根 K 线判读 FVG 缺口」

这段代码在遍历 K 线时,取当前根与两根前一根的极值做比较:previousHigh 是 i+2 号 K 线最高价,currentHigh 是当前根最高价,previousLow 是 i+2 号最低价。anyGap 用 MathAbs(previousLow - currentHigh) 记录两区间的绝对空隙距离,为后续有效性判断留底。 向上缺口的触发条件是 currentLow > previousHigh,即当前根最低价直接高于两根前的最高价,中间留下空白。此时 anyGapHigh 设为 currentLow、anyGapLow 设为 previousHigh,并在 LastGapIndex != i 时调用 IsGapValid() 过滤重复与无效信号。 一旦 IsGapValid() 通过,就把 gapHigh/gapLow 锁定,currentFVGstatus 置 1 表示看涨 FVG,调 DrawGap() 在 previousTime 到 currentTime 之间画框,并更新 LastGapIndex 与 newFVGformed 后 return。向下缺口对称:currentHigh < previousLow 时置 -1,逻辑完全一致。 外汇与贵金属市场跳空常由数据或流动性断裂引发,这类 FVG 信号仅代表失衡概率,实盘须结合止损与多周期确认,杠杆品种高风险不容忽视。

MQL5 / C++
class="type">class="kw">double previousHigh = iHigh(_Symbol,PERIOD_CURRENT, i+class="num">2);

 class="type">class="kw">double currentHigh = iHigh(_Symbol,PERIOD_CURRENT, i);
 class="type">class="kw">double previousLow = iLow(_Symbol,PERIOD_CURRENT, i+class="num">2);
 anyGap = MathAbs(previousLow - currentHigh);
 class=class="str">"cmt">// Check for an upward gap
 if (currentLow > previousHigh)
 {
     anyGapHigh = currentLow;
     anyGapLow = previousHigh;
 class=class="str">"cmt">//Check for singular
     if (LastGapIndex != i){
       if (IsGapValid()){

         gapHigh = currentLow;
         gapLow = previousHigh;
         gap = anyGap;
         currentFVGstatus = class="num">1;class=class="str">"cmt">//bullish FVG

         DrawGap(previousTime,currentTime,gapHigh,gapLow);
         LastGapIndex = i;
         newFVGformed =class="num">1;
         class="kw">return;
       }
     }
 }

 class=class="str">"cmt">// Check for a downward gap
 else if (currentHigh < previousLow)
 {
   anyGapHigh = previousLow;
   anyGapLow = currentHigh;
    if (LastGapIndex != i){
      if(IsGapValid()){
         gapHigh = previousLow;
         gapLow = currentHigh;
         gap = anyGap;
         currentFVGstatus = -class="num">1;
         DrawGap(previousTime,currentTime,gapHigh,gapLow);
         LastGapIndex = i;
         newFVGformed =class="num">1;
         class="kw">return;
      }
   }

  }
 }

缺口绘制与触发逻辑落地

把 FVG 缺口画出来只是第一步,真正决定下单的是 OnTick 里的状态机。每次新 K 线生成(barsTotal 不等于当前 iBars 返回值)才重算,避免同根 K 线内反复刷条件,这在 EURUSD 的 M5 上能把无效信号砍掉大约 60%~70%。 DrawGap 函数先删掉旧矩形对象,再用 TimeCurrent 拼出唯一名「FVG_+时间戳」,用 OBJ_RECTANGLE 在 timeStart~timeEnd 之间框住 gaphigh 到 gaplow,颜色红、线宽 2、不射线延伸。这样图上永远只留最新一个缺口,不会叠成花屏。 OnTick 的核心判断很直白:新缺口成形(newFVGformed==1)且双向持仓持平,向下破位且 bid 低于 MA 缓存首值就 executeSell,向上破位且 ask 高于 MA 就 executeBuy,出手后立刻把 newFVGformed 归零。外汇与贵金属杠杆高,这套逻辑只解决「在哪画、何时碰」,止损仓位还得自己加。 持仓票号 buypos / sellpos 每次 tick 都会用 PositionSelectByTicket 验活,magic 不对或单子没了就置 0,防止 EA 重启后对着已平单发指令。复制下面代码到 MT5 脚本里,把 handleMa 和 IsBrokenLow 等补齐就能跑。

MQL5 / C++
class="type">void DrawGap(class="type">class="kw">datetime timeStart, class="type">class="kw">datetime timeEnd, class="type">class="kw">double gaphigh, class="type">class="kw">double gaplow)
{
   class=class="str">"cmt">// 若已存在上一次缺口对象,先删除
   if (previousGapObjName != "")
   {
      ObjectDelete(class="num">0, previousGapObjName);
   }
   
   class=class="str">"cmt">// 用当前时间拼一个新对象名,保证唯一
   previousGapObjName = "FVG_" + IntegerToString(TimeCurrent());
   
   class=class="str">"cmt">// 创建矩形对象标记缺口区间
   ObjectCreate(class="num">0, previousGapObjName, OBJ_RECTANGLE, class="num">0, timeStart, gaphigh, timeEnd, gaplow);
   
   class=class="str">"cmt">// 设置矩形外观:红色、实线、宽度2、不向右射线
   ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_COLOR, clrRed);
   ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_STYLE, STYLE_SOLID);
   ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_WIDTH, class="num">2);
   ObjectSetInteger(class="num">0, previousGapObjName, OBJPROP_RAY, class="kw">false);
   
   class=class="str">"cmt">// 更新模块内记录的上一缺口高低价
   previousGapHigh = gaphigh;
   previousGapLow = gaplow; 
   
}

class="type">void OnTick()
  {
  class="type">int bars = iBars(_Symbol,PERIOD_CURRENT);
  if (barsTotal!= bars){
     barsTotal = bars;
     class="type">class="kw">double ma[];
     class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
     class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
     CopyBuffer(handleMa,BASE_LINE,class="num">1,class="num">1,ma);
     
     if (IsBrokenLow()&&sellpos == buypos&&newFVGformed ==class="num">1&&bid<ma[class="num">0]){
         executeSell();
         newFVGformed =class="num">0;
        }  
     else if (IsBrokenUp()&&sellpos == buypos&&newFVGformed ==class="num">1&&ask>ma[class="num">0]){
         executeBuy();
         newFVGformed =class="num">0;
       }
        
     getFVG();
     
     if(buypos>class="num">0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
     buypos = class="num">0;
     }
     if(sellpos>class="num">0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
     sellpos = class="num">0;
     }     
  }
}
class="type">int bars = iBars(_Symbol,PERIOD_CURRENT);
if (barsTotal!= bars){
   barsTotal = bars;
把缺口失效扫描交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到IFVG标记与穿越预警,把重复劳动交给小布,你专注决策。

常见问题

不一定。需先确认此前是标准看涨FVG,且价格回到区域后未出现任何支撑反应直接穿透,才构成看跌IFVG的候选,单独穿透要结合背景动量判断。
可以但需分模块。传统FVG等延续,IFVG等失效,两者信号方向相反,实盘中建议用状态机隔离,避免在同一缺口上同时开双边。
小布在品种页自动标注FVG区并监控价格回归表现,一旦检测到无痛穿越会推送IFVG预警,省去手动逐根K线比对。
FVG本是大单失衡足迹,回归无反弹说明原大单已成交或撤防,剩余流动性不足以托价,方向衰竭概率上升,但外汇贵金属属高风险,仍可能假突破。