//+------------------------------------------------------------------+ //| 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 { ENUM_FVG_TYPE type; ENUM_FVG_STATUS status; double upperEdge; double lowerEdge; double slReferencePrice; datetime createdTime; int ageInBars; }; //+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+ //| Helper: status helpers & impulse strength | //+------------------------------------------------------------------+ bool IsZoneActive(const FVGZone &z) { return (z.status != EXPIRED); } bool IsZoneLive(const FVGZone &z) { return (z.status == ACTIVE || z.status == TOUCHED); } bool IsZoneMitigated(const FVGZone &z) { return (z.status == MITIGATED); } bool IsZoneTouched(const FVGZone &z) { return (z.status == TOUCHED); } bool IsZoneExpired(const FVGZone &z) { return (z.status == EXPIRED); } // Impulse candle strength: 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(IsZoneActive(g_FVGZones[i]) && 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, double slReference, 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].type = type; g_FVGZones[g_FVGCount].status = ACTIVE; g_FVGZones[g_FVGCount].upperEdge = upper; g_FVGZones[g_FVGCount].lowerEdge = lower; g_FVGZones[g_FVGCount].slReferencePrice = slReference; g_FVGZones[g_FVGCount].createdTime = time; g_FVGZones[g_FVGCount].ageInBars = 0; 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)) { double slRef = iLow(symbol, InpTimeframe, shiftB); // bar2.low AddFVGZone(FVG_BULLISH, candleC_Low, candleA_High, slRef, 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)) { double slRef = iHigh(symbol, InpTimeframe, shiftB); // bar2.high AddFVGZone(FVG_BEARISH, candleA_Low, candleC_High, slRef, 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(IsZoneMitigated(g_FVGZones[i]) || !IsZoneActive(g_FVGZones[i])) continue; double lastLow = iLow (symbol, InpTimeframe, 1); double lastHigh = iHigh(symbol, InpTimeframe, 1); double zoneHeight = g_FVGZones[i].upperEdge - g_FVGZones[i].lowerEdge; if(zoneHeight <= 0) continue; //--- Bullish FVG: price comes down into the gap --- if(g_FVGZones[i].type == FVG_BULLISH) { // Mitigated: price drops through lower edge if(lastLow <= g_FVGZones[i].lowerEdge) { g_FVGZones[i].status = MITIGATED; if(InpDebugLog) PrintFormat("[FVG] MITIGATED BULL [%.5f – %.5f]", g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); continue; } // Touched: price has filled at least X% of gap, but not fully double deepestPrice = MathMin(lastLow, g_FVGZones[i].upperEdge); deepestPrice = MathMax(deepestPrice, g_FVGZones[i].lowerEdge); double fillPct = (g_FVGZones[i].upperEdge - deepestPrice) / zoneHeight * 100.0; if(g_FVGZones[i].status == ACTIVE && fillPct >= InpFVGTouchedPercent) g_FVGZones[i].status = TOUCHED; } //--- Bearish FVG: price goes up into the gap --- if(g_FVGZones[i].type == FVG_BEARISH) { // Mitigated: price rises through upper edge if(lastHigh >= g_FVGZones[i].upperEdge) { g_FVGZones[i].status = MITIGATED; if(InpDebugLog) PrintFormat("[FVG] MITIGATED BEAR [%.5f – %.5f]", g_FVGZones[i].lowerEdge, g_FVGZones[i].upperEdge); continue; } // Touched: price has filled at least X% of gap, but not fully double highestPrice = MathMax(lastHigh, g_FVGZones[i].lowerEdge); highestPrice = MathMin(highestPrice, g_FVGZones[i].upperEdge); double fillPct = (highestPrice - g_FVGZones[i].lowerEdge) / zoneHeight * 100.0; if(g_FVGZones[i].status == ACTIVE && fillPct >= InpFVGTouchedPercent) g_FVGZones[i].status = TOUCHED; } } } //+------------------------------------------------------------------+ //| Age out old FVGs beyond max age | //+------------------------------------------------------------------+ void ExpireOldFVGs() { for(int i = 0; i < g_FVGCount; i++) { if(!IsZoneActive(g_FVGZones[i])) continue; g_FVGZones[i].ageInBars++; if(g_FVGZones[i].ageInBars > InpFVGMaxAgeBars) { g_FVGZones[i].status = EXPIRED; 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(IsZoneActive(g_FVGZones[i])) { 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(IsZoneActive(g_FVGZones[i]) && g_FVGZones[i].type == type) count++; } return count; } int CountMitigatedFVGs() { int count = 0; for(int i = 0; i < g_FVGCount; i++) { if(IsZoneMitigated(g_FVGZones[i])) count++; } return count; } #endif