//+------------------------------------------------------------------+ //| FVG.mqh – Step 2: Fair Value Gap detection & management | //| | //| 3-candle pattern: | //| CandleA (oldest) ── CandleB (impulse) ── CandleC (newest) | //| | //| Bullish FVG: CandleA.High < CandleC.Low → gap UP | //| zone = [CandleA.High .. CandleC.Low] | //| CandleB must be bullish with strong body | //| | //| Bearish FVG: CandleA.Low > CandleC.High → gap DOWN | //| zone = [CandleC.High .. CandleA.Low] | //| CandleB must be bearish with strong body | //+------------------------------------------------------------------+ #ifndef __SIMPLE_FVG_FVG_MQH__ #define __SIMPLE_FVG_FVG_MQH__ #include "Config.mqh" //+------------------------------------------------------------------+ //| FVG data structure | //+------------------------------------------------------------------+ struct FVGZone { bool isActive; ENUM_FVG_TYPE type; double upperEdge; double lowerEdge; datetime createdTime; int ageInBars; bool isMitigated; }; //+------------------------------------------------------------------+ //| Module state | //+------------------------------------------------------------------+ FVGZone g_FVGZones[]; int g_FVGCount = 0; //+------------------------------------------------------------------+ //| Init: allocate array | //+------------------------------------------------------------------+ void FVGInit() { ArrayResize(g_FVGZones, MAX_FVG_SLOTS); for(int i = 0; i < MAX_FVG_SLOTS; i++) ZeroMemory(g_FVGZones[i]); g_FVGCount = 0; } //+------------------------------------------------------------------+ //| Check if impulse candle has strong body (body/range >= threshold) | //+------------------------------------------------------------------+ bool IsImpulseCandleStrong(string symbol, ENUM_TIMEFRAMES tf, int shift) { double high = iHigh(symbol, tf, shift); double low = iLow(symbol, tf, shift); double open = iOpen(symbol, tf, shift); double close = iClose(symbol, tf, shift); double totalRange = high - low; if(totalRange < _Point) return false; double bodySize = MathAbs(close - open); double bodyRatio = bodySize / totalRange * 100.0; return (bodyRatio >= InpFVGMinBodyPct); } //+------------------------------------------------------------------+ //| Check if this FVG timestamp already exists in our array | //+------------------------------------------------------------------+ bool FVGAlreadyTracked(datetime fvgTime, ENUM_FVG_TYPE fvgType) { for(int i = 0; i < g_FVGCount; i++) { if(g_FVGZones[i].isActive && g_FVGZones[i].createdTime == fvgTime && g_FVGZones[i].type == fvgType) return true; } return false; } //+------------------------------------------------------------------+ //| Add a new FVG zone to the array | //+------------------------------------------------------------------+ bool AddFVGZone(ENUM_FVG_TYPE type, double upper, double lower, datetime time) { if(g_FVGCount >= MAX_FVG_SLOTS) return false; if(InpFVGMinSizePoints > 0) { double gapSizePoints = (upper - lower) / _Point; if(gapSizePoints < InpFVGMinSizePoints) return false; } ZeroMemory(g_FVGZones[g_FVGCount]); g_FVGZones[g_FVGCount].isActive = true; g_FVGZones[g_FVGCount].type = type; g_FVGZones[g_FVGCount].upperEdge = upper; g_FVGZones[g_FVGCount].lowerEdge = lower; g_FVGZones[g_FVGCount].createdTime = time; g_FVGZones[g_FVGCount].ageInBars = 0; g_FVGZones[g_FVGCount].isMitigated = false; g_FVGCount++; if(InpDebugLog) PrintFormat("[FVG] NEW %s [%.5f – %.5f] at %s", (type == FVG_BULLISH) ? "BULL" : "BEAR", lower, upper, TimeToString(time)); return true; } //+------------------------------------------------------------------+ //| Scan for new FVG patterns across the lookback range | //| Finds ALL FVGs regardless of current trend | //+------------------------------------------------------------------+ void ScanForNewFVGs() { string symbol = GetTradeSymbol(); int totalBars = Bars(symbol, InpTimeframe); int maxShift = MathMin(InpFVGLookbackBars, totalBars - 3); for(int shift = 1; shift <= maxShift; shift++) { int shiftA = shift + 2; // oldest candle int shiftB = shift + 1; // impulse (middle) candle int shiftC = shift; // newest candle double candleA_High = iHigh(symbol, InpTimeframe, shiftA); double candleA_Low = iLow (symbol, InpTimeframe, shiftA); double candleB_Open = iOpen (symbol, InpTimeframe, shiftB); double candleB_Close = iClose(symbol, InpTimeframe, shiftB); double candleC_High = iHigh(symbol, InpTimeframe, shiftC); double candleC_Low = iLow (symbol, InpTimeframe, shiftC); datetime fvgTime = iTime(symbol, InpTimeframe, shiftC); //--- Bullish FVG: gap between A.High and C.Low --- if(candleA_High < candleC_Low && candleB_Close > candleB_Open && IsImpulseCandleStrong(symbol, InpTimeframe, shiftB)) { if(!FVGAlreadyTracked(fvgTime, FVG_BULLISH)) AddFVGZone(FVG_BULLISH, candleC_Low, candleA_High, fvgTime); } //--- Bearish FVG: gap between C.High and A.Low --- if(candleA_Low > candleC_High && candleB_Close < candleB_Open && IsImpulseCandleStrong(symbol, InpTimeframe, shiftB)) { if(!FVGAlreadyTracked(fvgTime, FVG_BEARISH)) AddFVGZone(FVG_BEARISH, candleA_Low, candleC_High, fvgTime); } } } //+------------------------------------------------------------------+ //| Check if price has mitigated (filled through) any active FVG | //+------------------------------------------------------------------+ void CheckMitigationStatus() { string symbol = GetTradeSymbol(); for(int i = 0; i < g_FVGCount; i++) { if(!g_FVGZones[i].isActive || g_FVGZones[i].isMitigated) continue; double lastLow = iLow (symbol, InpTimeframe, 1); double lastHigh = iHigh(symbol, InpTimeframe, 1); // Bullish FVG mitigated: price drops through lower edge if(g_FVGZones[i].type == FVG_BULLISH && lastLow <= g_FVGZones[i].lowerEdge) { g_FVGZones[i].isMitigated = true; if(InpDebugLog) PrintFormat("[FVG] MITIGATED BULL [%.5f – %.5f]", g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); } // Bearish FVG mitigated: price rises through upper edge if(g_FVGZones[i].type == FVG_BEARISH && lastHigh >= g_FVGZones[i].upperEdge) { g_FVGZones[i].isMitigated = true; if(InpDebugLog) PrintFormat("[FVG] MITIGATED BEAR [%.5f – %.5f]", g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); } } } //+------------------------------------------------------------------+ //| Age out old FVGs beyond max age | //+------------------------------------------------------------------+ void ExpireOldFVGs() { for(int i = 0; i < g_FVGCount; i++) { if(!g_FVGZones[i].isActive) continue; g_FVGZones[i].ageInBars++; if(g_FVGZones[i].ageInBars > InpFVGMaxAgeBars) { g_FVGZones[i].isActive = false; if(InpDebugLog) PrintFormat("[FVG] EXPIRED %s [%.5f – %.5f] age=%d bars", (g_FVGZones[i].type == FVG_BULLISH) ? "BULL" : "BEAR", g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge, g_FVGZones[i].ageInBars); } } } //+------------------------------------------------------------------+ //| Remove inactive FVGs from array to free slots | //+------------------------------------------------------------------+ void CompactFVGArray() { int writeIndex = 0; for(int i = 0; i < g_FVGCount; i++) { if(g_FVGZones[i].isActive) { if(writeIndex != i) g_FVGZones[writeIndex] = g_FVGZones[i]; writeIndex++; } } g_FVGCount = writeIndex; } //+------------------------------------------------------------------+ //| Main update: mitigation → expiry → scan new → compact | //+------------------------------------------------------------------+ void FVGUpdate() { CheckMitigationStatus(); ExpireOldFVGs(); ScanForNewFVGs(); CompactFVGArray(); } //+------------------------------------------------------------------+ //| Count helpers for panel display | //+------------------------------------------------------------------+ int CountActiveFVGs(ENUM_FVG_TYPE type) { int count = 0; for(int i = 0; i < g_FVGCount; i++) { if(g_FVGZones[i].isActive && g_FVGZones[i].type == type) count++; } return count; } int CountMitigatedFVGs() { int count = 0; for(int i = 0; i < g_FVGCount; i++) { if(g_FVGZones[i].isActive && g_FVGZones[i].isMitigated) count++; } return count; } #endif