//+------------------------------------------------------------------+ //| LiquiditySweep.mqh | //| Copyright 2024, MT5 Sniper Strategy Team | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MT5 Sniper Strategy Team" #property link "https://www.mql5.com" #include "../Utils/Logger.mqh" //+------------------------------------------------------------------+ //| Liquidity Zone Structure | //+------------------------------------------------------------------+ struct SLiquidityZone { datetime time; // Time of zone formation double price; // Price level of liquidity bool isHigh; // True for high liquidity, false for low bool isSwept; // Has been swept bool isValid; // Is the zone still valid int strength; // Strength of liquidity (1-5) double volume; // Volume at formation int touchCount; // Number of times price touched this level string timeframe; // Timeframe where zone was detected }; //+------------------------------------------------------------------+ //| Liquidity Sweep Structure | //+------------------------------------------------------------------+ struct SLiquiditySweep { datetime time; // Time of sweep double sweepPrice; // Price where sweep occurred double reversalPrice; // Price where reversal started bool isBullishSweep; // True for bullish sweep (sweep lows then up) bool isValid; // Is the sweep still valid bool isConfirmed; // Has the sweep been confirmed with reversal int strength; // Strength of the sweep (1-5) double sweepDistance; // Distance of the sweep string timeframe; // Timeframe where sweep was detected }; //+------------------------------------------------------------------+ //| Liquidity Sweep Detector Class | //+------------------------------------------------------------------+ class CLiquiditySweepDetector { private: string m_symbol; ENUM_TIMEFRAMES m_timeframe; CLogger* m_logger; SLiquidityZone m_liquidityZones[]; SLiquiditySweep m_sweeps[]; int m_maxZones; int m_maxSweeps; // Detection parameters int m_lookbackPeriod; double m_minSweepDistance; int m_reversalBars; double m_liquidityThreshold; bool m_useVolumeFilter; double m_volumeMultiplier; // Helper methods bool DetectLiquidityZones(); bool IsLiquidityLevel(int index, bool checkHigh); bool CheckForSweep(); bool IsBullishSweep(double sweepPrice, double currentPrice); bool IsBearishSweep(double sweepPrice, double currentPrice); int CalculateSweepStrength(const SLiquiditySweep &sweep); bool ConfirmSweep(SLiquiditySweep &sweep); void CleanupOldData(); SLiquidityZone GetNearestLiquidityZone(double price, bool isHigh); public: CLiquiditySweepDetector(); ~CLiquiditySweepDetector(); bool Initialize(string symbol, ENUM_TIMEFRAMES timeframe, CLogger* logger); void SetParameters(int lookback, double minSweepDistance, int reversalBars, double liquidityThreshold, bool useVolume, double volumeMultiplier); bool DetectSweeps(); int GetSweepCount(); SLiquiditySweep GetSweep(int index); SLiquiditySweep GetLatestSweep(bool bullish); bool IsRecentBullishSweep(int lookbackBars = 10); bool IsRecentBearishSweep(int lookbackBars = 10); bool HasValidSweep(bool checkBullish = true, bool checkBearish = true); // Liquidity analysis double GetNearestLiquidityHigh(); double GetNearestLiquidityLow(); bool IsLiquidityZone(double price, double tolerance = 0.0001); int GetLiquidityZoneCount(); // Sweep validation bool IsSweepAndReverse(bool bullish); double GetSweepReversalLevel(bool bullish); // Visualization void DrawLiquidityZones(); void DrawSweeps(); void RemoveLiquidityObjects(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CLiquiditySweepDetector::CLiquiditySweepDetector() { m_symbol = ""; m_timeframe = PERIOD_CURRENT; m_logger = NULL; m_maxZones = 30; m_maxSweeps = 20; // Default parameters m_lookbackPeriod = 20; m_minSweepDistance = 0.0001; m_reversalBars = 5; m_liquidityThreshold = 0.0005; m_useVolumeFilter = false; m_volumeMultiplier = 1.5; ArrayResize(m_liquidityZones, m_maxZones); ArrayResize(m_sweeps, m_maxSweeps); ArrayInitialize(m_liquidityZones, 0); ArrayInitialize(m_sweeps, 0); } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CLiquiditySweepDetector::~CLiquiditySweepDetector() { RemoveLiquidityObjects(); } //+------------------------------------------------------------------+ //| Initialize detector | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::Initialize(string symbol, ENUM_TIMEFRAMES timeframe, CLogger* logger) { m_symbol = symbol; m_timeframe = timeframe; m_logger = logger; if(m_logger != NULL) { m_logger->Info(StringFormat("Liquidity Sweep Detector initialized for %s on %s", m_symbol, EnumToString(m_timeframe))); } return true; } //+------------------------------------------------------------------+ //| Set detection parameters | //+------------------------------------------------------------------+ void CLiquiditySweepDetector::SetParameters(int lookback, double minSweepDistance, int reversalBars, double liquidityThreshold, bool useVolume, double volumeMultiplier) { m_lookbackPeriod = lookback; m_minSweepDistance = minSweepDistance; m_reversalBars = reversalBars; m_liquidityThreshold = liquidityThreshold; m_useVolumeFilter = useVolume; m_volumeMultiplier = volumeMultiplier; if(m_logger != NULL) { m_logger->Debug(StringFormat("Liquidity Parameters: Lookback=%d, MinSweep=%.5f, Reversal=%d", lookback, minSweepDistance, reversalBars)); } } //+------------------------------------------------------------------+ //| Detect liquidity sweeps | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::DetectSweeps() { if(m_symbol == "" || m_timeframe == PERIOD_CURRENT) return false; // First detect liquidity zones if(!DetectLiquidityZones()) return false; // Clean up old data CleanupOldData(); // Check for new sweeps return CheckForSweep(); } //+------------------------------------------------------------------+ //| Detect liquidity zones | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::DetectLiquidityZones() { int bars = iBars(m_symbol, m_timeframe); if(bars < m_lookbackPeriod + 10) return false; int zoneCount = 0; // Clear existing zones for(int i = 0; i < ArraySize(m_liquidityZones); i++) { m_liquidityZones[i].isValid = false; } // Detect liquidity levels (equal highs/lows, support/resistance) for(int i = 5; i < bars - 5 && zoneCount < m_maxZones; i++) { // Check for liquidity high if(IsLiquidityLevel(i, true)) { m_liquidityZones[zoneCount].time = iTime(m_symbol, m_timeframe, i); m_liquidityZones[zoneCount].price = iHigh(m_symbol, m_timeframe, i); m_liquidityZones[zoneCount].isHigh = true; m_liquidityZones[zoneCount].isSwept = false; m_liquidityZones[zoneCount].isValid = true; m_liquidityZones[zoneCount].volume = iVolume(m_symbol, m_timeframe, i); m_liquidityZones[zoneCount].touchCount = 1; m_liquidityZones[zoneCount].timeframe = EnumToString(m_timeframe); m_liquidityZones[zoneCount].strength = 1; zoneCount++; } // Check for liquidity low else if(IsLiquidityLevel(i, false)) { m_liquidityZones[zoneCount].time = iTime(m_symbol, m_timeframe, i); m_liquidityZones[zoneCount].price = iLow(m_symbol, m_timeframe, i); m_liquidityZones[zoneCount].isHigh = false; m_liquidityZones[zoneCount].isSwept = false; m_liquidityZones[zoneCount].isValid = true; m_liquidityZones[zoneCount].volume = iVolume(m_symbol, m_timeframe, i); m_liquidityZones[zoneCount].touchCount = 1; m_liquidityZones[zoneCount].timeframe = EnumToString(m_timeframe); m_liquidityZones[zoneCount].strength = 1; zoneCount++; } } // Calculate strength and touch count for each zone for(int i = 0; i < zoneCount; i++) { if(!m_liquidityZones[i].isValid) continue; int touches = 0; double zonePrice = m_liquidityZones[i].price; bool isHigh = m_liquidityZones[i].isHigh; // Count how many times price touched this level for(int j = 0; j < bars - 1; j++) { double high = iHigh(m_symbol, m_timeframe, j); double low = iLow(m_symbol, m_timeframe, j); if(isHigh) { if(MathAbs(high - zonePrice) <= m_liquidityThreshold) touches++; } else { if(MathAbs(low - zonePrice) <= m_liquidityThreshold) touches++; } } m_liquidityZones[i].touchCount = touches; m_liquidityZones[i].strength = MathMin(touches, 5); } if(m_logger != NULL) { m_logger->Debug(StringFormat("Detected %d liquidity zones", zoneCount)); } return zoneCount > 0; } //+------------------------------------------------------------------+ //| Check if level is a liquidity level | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::IsLiquidityLevel(int index, bool checkHigh) { if(index <= 2 || index >= iBars(m_symbol, m_timeframe) - 2) return false; double currentPrice = checkHigh ? iHigh(m_symbol, m_timeframe, index) : iLow(m_symbol, m_timeframe, index); int matches = 0; // Look for equal highs/lows within the lookback period for(int i = index - m_lookbackPeriod; i <= index + m_lookbackPeriod; i++) { if(i == index || i < 0 || i >= iBars(m_symbol, m_timeframe)) continue; double comparePrice = checkHigh ? iHigh(m_symbol, m_timeframe, i) : iLow(m_symbol, m_timeframe, i); if(MathAbs(currentPrice - comparePrice) <= m_liquidityThreshold) { matches++; } } // Need at least 2 matches to be considered liquidity return matches >= 2; } //+------------------------------------------------------------------+ //| Check for liquidity sweep | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::CheckForSweep() { double currentPrice = iClose(m_symbol, m_timeframe, 0); bool foundSweep = false; // Check each liquidity zone for potential sweep for(int i = 0; i < ArraySize(m_liquidityZones); i++) { if(!m_liquidityZones[i].isValid || m_liquidityZones[i].isSwept) continue; double zonePrice = m_liquidityZones[i].price; bool isHigh = m_liquidityZones[i].isHigh; // Check if price has swept through the liquidity zone bool swept = false; if(isHigh) { // For high liquidity, check if price went above and then reversed if(currentPrice > zonePrice + m_minSweepDistance) { // Check for reversal bool hasReversal = false; for(int j = 1; j <= m_reversalBars; j++) { double pastPrice = iClose(m_symbol, m_timeframe, j); if(pastPrice < zonePrice) { hasReversal = true; break; } } swept = hasReversal; } } else { // For low liquidity, check if price went below and then reversed if(currentPrice < zonePrice - m_minSweepDistance) { // Check for reversal bool hasReversal = false; for(int j = 1; j <= m_reversalBars; j++) { double pastPrice = iClose(m_symbol, m_timeframe, j); if(pastPrice > zonePrice) { hasReversal = true; break; } } swept = hasReversal; } } if(swept) { // Mark zone as swept m_liquidityZones[i].isSwept = true; // Create sweep signal SLiquiditySweep newSweep; newSweep.time = TimeCurrent(); newSweep.sweepPrice = zonePrice; newSweep.reversalPrice = currentPrice; newSweep.isBullishSweep = !isHigh; // Sweep lows = bullish, sweep highs = bearish newSweep.isValid = true; newSweep.isConfirmed = false; newSweep.sweepDistance = MathAbs(currentPrice - zonePrice); newSweep.timeframe = EnumToString(m_timeframe); newSweep.strength = CalculateSweepStrength(newSweep); // Add to array for(int j = 0; j < ArraySize(m_sweeps); j++) { if(!m_sweeps[j].isValid) { m_sweeps[j] = newSweep; foundSweep = true; break; } } if(foundSweep && m_logger != NULL) { m_logger->LogMarketStructure( StringFormat("%s Liquidity Sweep", newSweep.isBullishSweep ? "Bullish" : "Bearish"), m_symbol, newSweep.sweepPrice, newSweep.time ); } } } return foundSweep; } //+------------------------------------------------------------------+ //| Calculate sweep strength | //+------------------------------------------------------------------+ int CLiquiditySweepDetector::CalculateSweepStrength(const SLiquiditySweep &sweep) { int strength = 1; // Distance of sweep double atr = iATR(m_symbol, m_timeframe, 14, 1); if(atr > 0) { double sweepRatio = sweep.sweepDistance / atr; if(sweepRatio > 0.5) strength++; if(sweepRatio > 1.0) strength++; } // Volume confirmation if(m_useVolumeFilter) { double currentVolume = iVolume(m_symbol, m_timeframe, 0); double avgVolume = 0; for(int i = 1; i <= 10; i++) { avgVolume += iVolume(m_symbol, m_timeframe, i); } avgVolume /= 10; if(currentVolume > avgVolume * m_volumeMultiplier) strength++; } // Speed of reversal int reversalSpeed = 0; double startPrice = sweep.sweepPrice; double endPrice = sweep.reversalPrice; for(int i = 1; i <= 5; i++) { double price = iClose(m_symbol, m_timeframe, i); if(sweep.isBullishSweep) { if(price > startPrice) { reversalSpeed = 6 - i; // Faster reversal = higher score break; } } else { if(price < startPrice) { reversalSpeed = 6 - i; break; } } } if(reversalSpeed >= 4) strength++; return MathMin(strength, 5); } //+------------------------------------------------------------------+ //| Get nearest liquidity zone | //+------------------------------------------------------------------+ SLiquidityZone CLiquiditySweepDetector::GetNearestLiquidityZone(double price, bool isHigh) { SLiquidityZone nearestZone = {0}; double nearestDistance = DBL_MAX; for(int i = 0; i < ArraySize(m_liquidityZones); i++) { if(!m_liquidityZones[i].isValid || m_liquidityZones[i].isSwept) continue; if(m_liquidityZones[i].isHigh != isHigh) continue; double distance = MathAbs(price - m_liquidityZones[i].price); if(distance < nearestDistance) { nearestDistance = distance; nearestZone = m_liquidityZones[i]; } } return nearestZone; } //+------------------------------------------------------------------+ //| Clean up old data | //+------------------------------------------------------------------+ void CLiquiditySweepDetector::CleanupOldData() { datetime currentTime = TimeCurrent(); // Clean up old liquidity zones for(int i = 0; i < ArraySize(m_liquidityZones); i++) { if(m_liquidityZones[i].isValid) { if(currentTime - m_liquidityZones[i].time > PeriodSeconds(m_timeframe) * 100) { m_liquidityZones[i].isValid = false; } } } // Clean up old sweeps for(int i = 0; i < ArraySize(m_sweeps); i++) { if(m_sweeps[i].isValid) { if(currentTime - m_sweeps[i].time > PeriodSeconds(m_timeframe) * 50) { m_sweeps[i].isValid = false; } } } } //+------------------------------------------------------------------+ //| Get sweep count | //+------------------------------------------------------------------+ int CLiquiditySweepDetector::GetSweepCount() { int count = 0; for(int i = 0; i < ArraySize(m_sweeps); i++) { if(m_sweeps[i].isValid) count++; } return count; } //+------------------------------------------------------------------+ //| Get sweep by index | //+------------------------------------------------------------------+ SLiquiditySweep CLiquiditySweepDetector::GetSweep(int index) { SLiquiditySweep emptySweep = {0}; if(index < 0 || index >= ArraySize(m_sweeps)) return emptySweep; if(!m_sweeps[index].isValid) return emptySweep; return m_sweeps[index]; } //+------------------------------------------------------------------+ //| Get latest sweep | //+------------------------------------------------------------------+ SLiquiditySweep CLiquiditySweepDetector::GetLatestSweep(bool bullish) { SLiquiditySweep latestSweep = {0}; for(int i = 0; i < ArraySize(m_sweeps); i++) { if(m_sweeps[i].isValid && m_sweeps[i].isBullishSweep == bullish) { if(latestSweep.time == 0 || m_sweeps[i].time > latestSweep.time) { latestSweep = m_sweeps[i]; } } } return latestSweep; } //+------------------------------------------------------------------+ //| Check for recent bullish sweep | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::IsRecentBullishSweep(int lookbackBars = 10) { datetime cutoffTime = TimeCurrent() - PeriodSeconds(m_timeframe) * lookbackBars; for(int i = 0; i < ArraySize(m_sweeps); i++) { if(m_sweeps[i].isValid && m_sweeps[i].isBullishSweep && m_sweeps[i].time >= cutoffTime) { return true; } } return false; } //+------------------------------------------------------------------+ //| Check for recent bearish sweep | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::IsRecentBearishSweep(int lookbackBars = 10) { datetime cutoffTime = TimeCurrent() - PeriodSeconds(m_timeframe) * lookbackBars; for(int i = 0; i < ArraySize(m_sweeps); i++) { if(m_sweeps[i].isValid && !m_sweeps[i].isBullishSweep && m_sweeps[i].time >= cutoffTime) { return true; } } return false; } //+------------------------------------------------------------------+ //| Check if has valid sweep | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::HasValidSweep(bool checkBullish = true, bool checkBearish = true) { for(int i = 0; i < ArraySize(m_sweeps); i++) { if(!m_sweeps[i].isValid) continue; if(m_sweeps[i].isBullishSweep && checkBullish) return true; if(!m_sweeps[i].isBullishSweep && checkBearish) return true; } return false; } //+------------------------------------------------------------------+ //| Get nearest liquidity high | //+------------------------------------------------------------------+ double CLiquiditySweepDetector::GetNearestLiquidityHigh() { double currentPrice = iClose(m_symbol, m_timeframe, 0); SLiquidityZone nearestHigh = GetNearestLiquidityZone(currentPrice, true); return nearestHigh.isValid ? nearestHigh.price : 0; } //+------------------------------------------------------------------+ //| Get nearest liquidity low | //+------------------------------------------------------------------+ double CLiquiditySweepDetector::GetNearestLiquidityLow() { double currentPrice = iClose(m_symbol, m_timeframe, 0); SLiquidityZone nearestLow = GetNearestLiquidityZone(currentPrice, false); return nearestLow.isValid ? nearestLow.price : 0; } //+------------------------------------------------------------------+ //| Check if price is in liquidity zone | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::IsLiquidityZone(double price, double tolerance = 0.0001) { for(int i = 0; i < ArraySize(m_liquidityZones); i++) { if(!m_liquidityZones[i].isValid || m_liquidityZones[i].isSwept) continue; if(MathAbs(price - m_liquidityZones[i].price) <= tolerance) { return true; } } return false; } //+------------------------------------------------------------------+ //| Get liquidity zone count | //+------------------------------------------------------------------+ int CLiquiditySweepDetector::GetLiquidityZoneCount() { int count = 0; for(int i = 0; i < ArraySize(m_liquidityZones); i++) { if(m_liquidityZones[i].isValid && !m_liquidityZones[i].isSwept) count++; } return count; } //+------------------------------------------------------------------+ //| Check if sweep and reverse pattern | //+------------------------------------------------------------------+ bool CLiquiditySweepDetector::IsSweepAndReverse(bool bullish) { SLiquiditySweep latestSweep = GetLatestSweep(bullish); if(!latestSweep.isValid) return false; // Check if the sweep happened recently (within last 10 bars) datetime cutoffTime = TimeCurrent() - PeriodSeconds(m_timeframe) * 10; if(latestSweep.time < cutoffTime) return false; // Check if price is moving in the expected direction after sweep double currentPrice = iClose(m_symbol, m_timeframe, 0); if(bullish) { return currentPrice > latestSweep.sweepPrice; } else { return currentPrice < latestSweep.sweepPrice; } } //+------------------------------------------------------------------+ //| Get sweep reversal level | //+------------------------------------------------------------------+ double CLiquiditySweepDetector::GetSweepReversalLevel(bool bullish) { SLiquiditySweep latestSweep = GetLatestSweep(bullish); return latestSweep.isValid ? latestSweep.reversalPrice : 0; } //+------------------------------------------------------------------+ //| Draw liquidity zones | //+------------------------------------------------------------------+ void CLiquiditySweepDetector::DrawLiquidityZones() { for(int i = 0; i < ArraySize(m_liquidityZones); i++) { if(!m_liquidityZones[i].isValid) continue; string objName = StringFormat("LIQ_%s_%d", m_symbol, i); color zoneColor = m_liquidityZones[i].isSwept ? clrGray : (m_liquidityZones[i].isHigh ? clrRed : clrBlue); // Create horizontal line if(ObjectCreate(0, objName, OBJ_HLINE, 0, 0, m_liquidityZones[i].price)) { ObjectSetInteger(0, objName, OBJPROP_COLOR, zoneColor); ObjectSetInteger(0, objName, OBJPROP_STYLE, m_liquidityZones[i].isSwept ? STYLE_DOT : STYLE_DASH); ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); ObjectSetString(0, objName, OBJPROP_TOOLTIP, StringFormat("Liquidity %s (Strength: %d, Touches: %d)", m_liquidityZones[i].isHigh ? "High" : "Low", m_liquidityZones[i].strength, m_liquidityZones[i].touchCount)); } } ChartRedraw(); } //+------------------------------------------------------------------+ //| Draw sweeps | //+------------------------------------------------------------------+ void CLiquiditySweepDetector::DrawSweeps() { for(int i = 0; i < ArraySize(m_sweeps); i++) { if(!m_sweeps[i].isValid) continue; string objName = StringFormat("SWEEP_%s_%d", m_symbol, i); color sweepColor = m_sweeps[i].isBullishSweep ? clrLime : clrRed; // Create arrow object if(ObjectCreate(0, objName, OBJ_ARROW, 0, m_sweeps[i].time, m_sweeps[i].sweepPrice)) { ObjectSetInteger(0, objName, OBJPROP_COLOR, sweepColor); ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, m_sweeps[i].isBullishSweep ? 241 : 242); ObjectSetInteger(0, objName, OBJPROP_WIDTH, 3); ObjectSetString(0, objName, OBJPROP_TOOLTIP, StringFormat("%s Liquidity Sweep (Strength: %d)", m_sweeps[i].isBullishSweep ? "Bullish" : "Bearish", m_sweeps[i].strength)); } } ChartRedraw(); } //+------------------------------------------------------------------+ //| Remove liquidity objects | //+------------------------------------------------------------------+ void CLiquiditySweepDetector::RemoveLiquidityObjects() { string liqPrefix = StringFormat("LIQ_%s_", m_symbol); string sweepPrefix = StringFormat("SWEEP_%s_", m_symbol); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string objName = ObjectName(0, i); if(StringFind(objName, liqPrefix) == 0 || StringFind(objName, sweepPrefix) == 0) { ObjectDelete(0, objName); } } ChartRedraw(); }