//+------------------------------------------------------------------+ //| LiquidityTrapEA.mq5 | //| Bachir live XAU/BTC (+ SP500 map) — inducement + liquidity trap | //| | //| Strategy map (language from Bachir charts): | //| ATH chase / BOS – break of structure toward an obvious | //| target (e.g. ATH) induces early buyers | //| or sellers into the move. | //| Inducement – BOS + liquidity below/above (LB) draws | //| participants; they form the trap. | //| Lexicon (charts) – SI+BT = Sell Inducement + Buy Trap → LONG | //| (sellers induced at low/flush; bounce). | //| BI+ST = Buy Inducement + Sell Trap → SHORT | //| (buyers induced at high/ATH; drop). | //| Range trap – consolidation high/low = LQ pools. | //| HARD GATE: no bias / no trade until M30 | //| range HIGH or LOW is actually TAKEN | //| (swept). "Better stay out until either | //| high or low is taken." Respect ≠ take: | //| if high is merely respected, do NOT short | //| — wait for the take. | //| High TAKEN before low → BI+ST / SELL. | //| Low TAKEN first → SI+BT / BUY. | //| LB – opposing liquidity (range low for shorts, | //| range high for longs); conceptual TP zone. | //| Entry (v1.20) – default: M5 CHOCH after M30 extreme sweep. | //| optional: direct M5 wick-reject at induce. | //| soft: PreferQuickScalp when HTFRangeWide. | //| HTF M30 range/LQ · LTF M5 sweep+CHOCH · SL beyond sweep · 2R | //+------------------------------------------------------------------+ #property copyright "Bachir Liquidity Trap Strategy" #property link "" #property version "1.20" #include //==================================================================== // INPUTS //==================================================================== input group "=== Core ===" input ulong InpMagicNumber = 20261002; // Magic number input int InpSlippage = 30; // Slippage (points) input double InpSL_BufferPoints = 50.0; // SL buffer beyond swept extreme (points) input double InpRiskReward = 2.0; // Take-profit R multiple (default 2R) input string InpTradeComment = "LiqTrap"; // Trade comment input group "=== Lot sizing ===" input double InpLotXAU = 0.02; // Lot when symbol contains XAU/GOLD input double InpLotBTC = 0.05; // Lot when symbol contains BTC/Bitcoin input double InpLotDefault = 0.01; // Lot for other symbols input bool InpLotOverride = false; // Force use of LotDefault for all symbols input group "=== Range (M30) ===" input int InpRangeLookbackBars = 40; // M30 bars to scan for swings / range input int InpSwingStrength = 3; // Bars left+right to confirm a swing input int InpMinRangeBars = 6; // Min M30 bars between swing high & low input double InpMinRangePoints = 100.0; // Min range height in points (filter noise) input group "=== Filters ===" input int InpMaxSpreadPoints = 80; // Max allowed spread (points); 0 = off input bool InpEnableXAU = true; // Allow attach on XAU/GOLD symbols input bool InpEnableBTC = true; // Allow attach on BTC/Bitcoin symbols input bool InpAllowOtherSymbols = true; // Allow attach on other symbols input group "=== M5 CHOCH / structure ===" input int InpM5SwingStrength = 2; // M5 swing strength for CHOCH input int InpM5Lookback = 30; // M5 bars to scan for CHOCH structure input group "=== Entry modes ===" input bool InpAllowDirectInduceEntry = false; // Allow M5 wick-reject at M30 extreme (no CHOCH) input double InpDirectRejectBodyPct = 0.35; // Min body fraction closing back inside range (0-1) input group "=== Soft HTF stance (optional) ===" input bool InpPreferQuickScalp = false; // Soft: when HTF range wide / no confluence → LTF scalp stance (default off) input double InpWideRangeMult = 3.0; // Range height ≥ MinRangePoints × this → HTFRangeWide input group "=== Display ===" input bool InpDrawRangeObjects = true; // Draw range high/low lines on chart input color InpColorRangeHigh = clrDodgerBlue; input color InpColorRangeLow = clrOrangeRed; //==================================================================== // GLOBALS //==================================================================== CTrade g_trade; enum ENUM_BIAS { BIAS_NONE = 0, BIAS_SELL, // BI+ST: high/ATH-side LQ TAKEN first → buy inducement + sell trap → short BIAS_BUY // SI+BT: low/LB TAKEN first → sell inducement + buy trap → long toward high }; enum ENUM_STATE { ST_IDLE = 0, // build / refresh M30 range (BOS/induce context box) ST_WAIT_SWEEP, // HARD GATE: stay here until high OR low is TAKEN (not merely respected) ST_WAIT_CHOCH, // extreme swept; wait CHOCH and/or direct induce reject ST_IN_TRADE // one position open (magic+symbol) }; double g_rangeHigh = 0.0; double g_rangeLow = 0.0; datetime g_rangeHighTime = 0; datetime g_rangeLowTime = 0; bool g_rangeValid = false; ENUM_BIAS g_bias = BIAS_NONE; ENUM_STATE g_state = ST_IDLE; double g_sweptLevel = 0.0; // the extreme that was taken (induce high/low) datetime g_sweepTime = 0; string g_entryMode = ""; // "CHOCH" or "DirectInduce" datetime g_lastM5Bar = 0; datetime g_lastM30Bar = 0; string g_objHigh = "LT_RangeHigh"; string g_objLow = "LT_RangeLow"; string g_objLabel = "LT_Status"; //==================================================================== // HELPERS //==================================================================== bool SymbolAllowed() { string s = _Symbol; string u = s; StringToUpper(u); bool isXau = (StringFind(u, "XAU") >= 0 || StringFind(u, "GOLD") >= 0); bool isBtc = (StringFind(u, "BTC") >= 0 || StringFind(u, "BITCOIN") >= 0); if(isXau) return InpEnableXAU; if(isBtc) return InpEnableBTC; return InpAllowOtherSymbols; } double ResolveLot() { if(InpLotOverride) return NormalizeLot(InpLotDefault); string u = _Symbol; StringToUpper(u); if(StringFind(u, "XAU") >= 0 || StringFind(u, "GOLD") >= 0) return NormalizeLot(InpLotXAU); if(StringFind(u, "BTC") >= 0 || StringFind(u, "BITCOIN") >= 0) return NormalizeLot(InpLotBTC); return NormalizeLot(InpLotDefault); } double NormalizeLot(double lot) { double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); if(step <= 0.0) step = 0.01; lot = MathFloor(lot / step + 1e-12) * step; if(lot < minLot) lot = minLot; if(lot > maxLot) lot = maxLot; int digits = (int)MathMax(0, MathCeil(-MathLog10(step))); return NormalizeDouble(lot, digits); } bool IsNewBar(ENUM_TIMEFRAMES tf, datetime &lastBar) { datetime t = iTime(_Symbol, tf, 0); if(t == 0) return false; if(t != lastBar) { lastBar = t; return true; } return false; } bool SpreadOk() { if(InpMaxSpreadPoints <= 0) return true; long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); return (spread <= InpMaxSpreadPoints); } // Soft HTF flag: wide M30 box / trap-zone with weak confluence → README // guidance is LTF scalps only. PreferQuickScalp (default false) only // annotates stance; it does not override the hard take-gate or core entry. bool IsHTFRangeWide() { if(!g_rangeValid) return false; if(InpWideRangeMult <= 0.0) return false; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(point <= 0.0) point = _Point; double heightPts = (g_rangeHigh - g_rangeLow) / point; return (heightPts >= InpMinRangePoints * InpWideRangeMult); } bool SoftScalpStance() { return (InpPreferQuickScalp && IsHTFRangeWide()); } bool HasOpenPosition() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; return true; } return false; } int CopyRatesSafe(ENUM_TIMEFRAMES tf, int count, MqlRates &rates[]) { ArraySetAsSeries(rates, true); int n = CopyRates(_Symbol, tf, 0, count, rates); return n; } //-------------------------------------------------------------------- // Swing detection: bar `i` is a swing high if High[i] is strictly // greater than High of `strength` bars on each side (i-strength..i-1 // and i+1..i+strength). Same for swing low with Low. // rates[] must be series (index 0 = newest). //-------------------------------------------------------------------- bool IsSwingHigh(const MqlRates &rates[], int i, int strength, int total) { if(i < strength || i + strength >= total) return false; double h = rates[i].high; for(int k = 1; k <= strength; k++) { if(rates[i - k].high >= h) return false; // newer side if(rates[i + k].high >= h) return false; // older side } return true; } bool IsSwingLow(const MqlRates &rates[], int i, int strength, int total) { if(i < strength || i + strength >= total) return false; double l = rates[i].low; for(int k = 1; k <= strength; k++) { if(rates[i - k].low <= l) return false; if(rates[i + k].low <= l) return false; } return true; } // Find most recent confirmed swing high/low in [fromIdx..toIdx] (series). // Returns bar index or -1. int FindNearestSwingHigh(const MqlRates &rates[], int fromIdx, int toIdx, int strength, int total) { int best = -1; for(int i = fromIdx; i <= toIdx; i++) { if(IsSwingHigh(rates, i, strength, total)) { best = i; break; // nearest (newest) first because series + ascending i from fromIdx } } return best; } int FindNearestSwingLow(const MqlRates &rates[], int fromIdx, int toIdx, int strength, int total) { int best = -1; for(int i = fromIdx; i <= toIdx; i++) { if(IsSwingLow(rates, i, strength, total)) { best = i; break; } } return best; } // Among confirmed swings in window, pick the highest high / lowest low // that form a consolidation box = inducement range / LQ pools. // Range HIGH ≈ liquidity above (buy-stops / ATH-side chase). // Range LOW ≈ LB liquidity below (sell-stops). First extreme taken // sets the trap bias (see DetectFirstSweep). bool BuildM30Range() { int need = InpRangeLookbackBars + InpSwingStrength * 2 + 5; MqlRates rates[]; int n = CopyRatesSafe(PERIOD_M30, need, rates); if(n < InpSwingStrength * 2 + InpMinRangeBars + 5) return false; int fromIdx = InpSwingStrength; // skip forming bars at tip int toIdx = MathMin(n - InpSwingStrength - 1, InpRangeLookbackBars); // Collect swings in window int hiIdx = -1; int loIdx = -1; double hiPrice = -DBL_MAX; double loPrice = DBL_MAX; // Prefer: most recent swing high and most recent swing low, // then verify they form a usable box. If the newer of the two // "invalidates" the older (e.g. swing high below swing low), // fall back to extremal swings in the lookback. int nearHi = FindNearestSwingHigh(rates, fromIdx, toIdx, InpSwingStrength, n); int nearLo = FindNearestSwingLow (rates, fromIdx, toIdx, InpSwingStrength, n); if(nearHi < 0 || nearLo < 0) return false; // Also find absolute swing extremes in window for a solid box for(int i = fromIdx; i <= toIdx; i++) { if(IsSwingHigh(rates, i, InpSwingStrength, n)) { if(rates[i].high > hiPrice) { hiPrice = rates[i].high; hiIdx = i; } } if(IsSwingLow(rates, i, InpSwingStrength, n)) { if(rates[i].low < loPrice) { loPrice = rates[i].low; loIdx = i; } } } if(hiIdx < 0 || loIdx < 0) return false; // Use extremal swings as liquidity pools (range high / range low) double rh = rates[hiIdx].high; double rl = rates[loIdx].low; if(rh <= rl) return false; int barGap = MathAbs(hiIdx - loIdx); if(barGap < InpMinRangeBars) return false; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(point <= 0.0) point = _Point; if((rh - rl) / point < InpMinRangePoints) return false; // Sideways / consolidation check (v1): after both swings exist, // recent closes should mostly sit inside the box (inducement context). int inside = 0, checked = 0; int checkFrom = fromIdx; int checkTo = MathMin(toIdx, MathMax(hiIdx, loIdx)); for(int i = checkFrom; i <= checkTo && checked < 12; i++) { checked++; if(rates[i].close <= rh && rates[i].close >= rl) inside++; } // Soft filter: at least half of recent closes inside box if(checked >= 4 && inside * 2 < checked) return false; g_rangeHigh = rh; g_rangeLow = rl; g_rangeHighTime = rates[hiIdx].time; g_rangeLowTime = rates[loIdx].time; g_rangeValid = true; return true; } //-------------------------------------------------------------------- // Sweep / first LQ grab on M5 — HARD GATE before any bias or trade. // Bachir: "Better stay out until either high or low is taken." // // TAKEN = M5 wick pierces BEYOND the M30 extreme (strict > / <). // RESPECTED = price approaches / tags the extreme but does NOT pierce // → stay in WAIT_SWEEP; do NOT set SELL bias / short. // If high is merely respected, wait for the take. // // Range trap (lexicon): // High TAKEN before low → BI+ST → BIAS_SELL (expect drop). // Low TAKEN first → SI+BT → BIAS_BUY (bounce toward high). // First chronological pierce of an extreme sets bias (induce filled). //-------------------------------------------------------------------- bool DetectFirstSweep(ENUM_BIAS &outBias, double &outLevel, datetime &outTime) { if(!g_rangeValid) return false; MqlRates m5[]; int n = CopyRatesSafe(PERIOD_M5, 80, m5); if(n < 5) return false; // Only consider bars that closed after both swing times (range context) datetime rangeReady = MathMax(g_rangeHighTime, g_rangeLowTime); for(int i = n - 2; i >= 1; i--) // oldest → newest, skip current forming [0] { if(m5[i].time <= rangeReady) continue; // Strict pierce only — equal/tag = respect, not take bool sweptHigh = (m5[i].high > g_rangeHigh); bool sweptLow = (m5[i].low < g_rangeLow); // Whichever extreme is TAKEN FIRST chronologically sets bias if(sweptHigh && !sweptLow) { outBias = BIAS_SELL; // BI+ST outLevel = g_rangeHigh; outTime = m5[i].time; return true; } if(sweptLow && !sweptHigh) { outBias = BIAS_BUY; // SI+BT outLevel = g_rangeLow; outTime = m5[i].time; return true; } if(sweptHigh && sweptLow) { // Same bar swept both — use which wick went further beyond double above = m5[i].high - g_rangeHigh; double below = g_rangeLow - m5[i].low; if(above >= below) { outBias = BIAS_SELL; // BI+ST outLevel = g_rangeHigh; } else { outBias = BIAS_BUY; // SI+BT outLevel = g_rangeLow; } outTime = m5[i].time; return true; } } return false; } //-------------------------------------------------------------------- // CHOCH (Change Of Character) on M5 after a TAKEN extreme (hard gate). // // Context (Bachir): BOS toward ATH / obvious target induces early // participants. They leave BOS + LB (liquidity below) or liquidity // above; the range extremes are the LQ pools we watch. // // After BEARISH sweep of range HIGH (BIAS_SELL / BI+ST) — buyers induced: // High TAKEN before low → sell trap. On M5: // 1) Locate a post-sweep M5 swing high (lower-high / trap wick) // 2) Locate the prior M5 swing low that formed before that high // 3) CHOCH = closed M5 candle breaks BELOW that prior swing low // → structure shifted bearish → enter SELL (TP toward LB / 2R). // // After BULLISH sweep of range LOW (BIAS_BUY / SI+BT) — sellers induced // (flush / LB grab; BTC long path = low-sweep + CHOCH): // Mirror: post-sweep M5 swing low, prior swing high, break ABOVE // that swing high on a closed M5 candle → CHOCH long → enter BUY. //-------------------------------------------------------------------- bool DetectChoCH(ENUM_BIAS bias, datetime afterTime) { MqlRates m5[]; int n = CopyRatesSafe(PERIOD_M5, InpM5Lookback + InpM5SwingStrength * 2 + 10, m5); if(n < InpM5SwingStrength * 2 + 8) return false; int strength = InpM5SwingStrength; int fromIdx = strength; // confirmed only int toIdx = MathMin(n - strength - 1, InpM5Lookback); // Restrict to swings that completed after the sweep (bar time > sweep) // Build list of post-sweep swing highs/lows int swingHi[]; int swingLo[]; ArrayResize(swingHi, 0); ArrayResize(swingLo, 0); for(int i = fromIdx; i <= toIdx; i++) { if(m5[i].time < afterTime) continue; if(IsSwingHigh(m5, i, strength, n)) { int sz = ArraySize(swingHi); ArrayResize(swingHi, sz + 1); swingHi[sz] = i; } if(IsSwingLow(m5, i, strength, n)) { int sz = ArraySize(swingLo); ArrayResize(swingLo, sz + 1); swingLo[sz] = i; } } // Current closed bar = index 1 (series) double close1 = m5[1].close; if(bias == BIAS_SELL) { // Need at least one post-sweep swing high and an older swing low // that sits before that high (higher index = older in series). if(ArraySize(swingHi) < 1) return false; int sh = swingHi[0]; // newest post-sweep swing high // Find a swing low that is older than this high (formed before it) int priorLo = -1; for(int j = 0; j < ArraySize(swingLo); j++) { if(swingLo[j] > sh) // older than swing high { priorLo = swingLo[j]; break; } } // Also allow a swing low found in a slightly wider pre-sweep window // if none post-sweep-but-older-than-high exists if(priorLo < 0) { for(int i = sh + 1; i <= toIdx; i++) { if(IsSwingLow(m5, i, strength, n)) { priorLo = i; break; } } } if(priorLo < 0) return false; double level = m5[priorLo].low; // CHOCH: closed M5 bar breaks that prior swing low if(close1 < level) return true; } else if(bias == BIAS_BUY) { if(ArraySize(swingLo) < 1) return false; int sl = swingLo[0]; // newest post-sweep swing low int priorHi = -1; for(int j = 0; j < ArraySize(swingHi); j++) { if(swingHi[j] > sl) { priorHi = swingHi[j]; break; } } if(priorHi < 0) { for(int i = sl + 1; i <= toIdx; i++) { if(IsSwingHigh(m5, i, strength, n)) { priorHi = i; break; } } } if(priorHi < 0) return false; double level = m5[priorHi].high; if(close1 > level) return true; } return false; } //-------------------------------------------------------------------- // Direct inducement entry (optional, InpAllowDirectInduceEntry). // LF example from Bachir SP500 map: wick into inducement level then // reject (e.g. short ~7718), SL above induce high, TP near LB / 2R. // // Rules on a CLOSED M5 bar (index 1), after bias already set by first // sweep of the M30 extreme: // SELL: high wicks ABOVE range high (induce), close back INSIDE // below range high with a meaningful body fraction inside. // BUY: low wicks BELOW range low, close back INSIDE above range low. // Does NOT require full CHOCH — faster trap entry when enabled. //-------------------------------------------------------------------- bool DetectDirectInduceRejection(ENUM_BIAS bias) { if(!InpAllowDirectInduceEntry) return false; if(!g_rangeValid || bias == BIAS_NONE) return false; MqlRates m5[]; if(CopyRatesSafe(PERIOD_M5, 5, m5) < 3) return false; // Only bars at/after the sweep (same bar allowed — wick-reject IS the trap) if(m5[1].time < g_sweepTime) return false; double o = m5[1].open; double c = m5[1].close; double h = m5[1].high; double l = m5[1].low; double range = h - l; if(range <= 0.0) return false; double bodyInsideFrac = InpDirectRejectBodyPct; if(bodyInsideFrac < 0.0) bodyInsideFrac = 0.0; if(bodyInsideFrac > 1.0) bodyInsideFrac = 1.0; if(bias == BIAS_SELL) { // Wick into inducement (above range high / ATH-side LQ), reject down if(h <= g_rangeHigh) return false; if(c >= g_rangeHigh) return false; // must close back inside / below induce high // Body mostly in direction of reject (bearish preference soft) double insideDepth = g_rangeHigh - c; if(insideDepth / range < bodyInsideFrac * 0.25) return false; // barely closed inside — weak reject // Prefer rejection candle: close in lower portion of bar or bearish bool bearish = (c < o); bool closeLowHalf = (c <= l + range * 0.55); if(!(bearish || closeLowHalf)) return false; return true; } else if(bias == BIAS_BUY) { if(l >= g_rangeLow) return false; if(c <= g_rangeLow) return false; double insideDepth = c - g_rangeLow; if(insideDepth / range < bodyInsideFrac * 0.25) return false; bool bullish = (c > o); bool closeHighHalf = (c >= h - range * 0.55); if(!(bullish || closeHighHalf)) return false; return true; } return false; } //-------------------------------------------------------------------- // Trade execution // SL beyond swept induce extreme (+ buffer). // TP fixed RiskReward (default 2R). Opposing pool (LB for shorts / // range high for longs) is the conceptual target zone on Bachir maps; // v1 does not force TP to LB — use RR so XAU/BTC sizing stays simple. //-------------------------------------------------------------------- bool OpenTrade(ENUM_BIAS bias) { if(HasOpenPosition()) return false; if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return false; if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) return false; if(!SpreadOk()) return false; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(point <= 0.0) point = _Point; int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); double lot = ResolveLot(); double buffer = InpSL_BufferPoints * point; double rr = (InpRiskReward > 0.0 ? InpRiskReward : 2.0); double price = 0.0, sl = 0.0, tp = 0.0; ENUM_ORDER_TYPE type; if(bias == BIAS_SELL) { price = SymbolInfoDouble(_Symbol, SYMBOL_BID); sl = g_sweptLevel + buffer; // beyond swept high if(sl <= price) sl = price + buffer; // safety double risk = sl - price; if(risk <= 0.0) return false; tp = price - risk * rr; type = ORDER_TYPE_SELL; } else if(bias == BIAS_BUY) { price = SymbolInfoDouble(_Symbol, SYMBOL_ASK); sl = g_sweptLevel - buffer; // beyond swept low if(sl >= price) sl = price - buffer; double risk = price - sl; if(risk <= 0.0) return false; tp = price + risk * rr; type = ORDER_TYPE_BUY; } else return false; price = NormalizeDouble(price, digits); sl = NormalizeDouble(sl, digits); tp = NormalizeDouble(tp, digits); // Stops level check long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); double minDist = stopsLevel * point; if(minDist > 0.0) { if(MathAbs(price - sl) < minDist) return false; if(MathAbs(tp - price) < minDist) return false; } g_trade.SetExpertMagicNumber(InpMagicNumber); g_trade.SetDeviationInPoints(InpSlippage); g_trade.SetTypeFillingBySymbol(_Symbol); string cmt = InpTradeComment; if(StringLen(g_entryMode) > 0) cmt = InpTradeComment + ":" + g_entryMode; bool ok = false; if(type == ORDER_TYPE_BUY) ok = g_trade.Buy(lot, _Symbol, price, sl, tp, cmt); else ok = g_trade.Sell(lot, _Symbol, price, sl, tp, cmt); if(!ok) { Print("LiquidityTrapEA: order failed — retcode=", g_trade.ResultRetcode(), " ", g_trade.ResultRetcodeDescription()); return false; } Print("LiquidityTrapEA: opened ", EnumToString(type), " mode=", g_entryMode, " lot=", lot, " price=", price, " SL=", sl, " TP=", tp, " induce/swept=", g_sweptLevel, " LB/opp=", (bias == BIAS_SELL ? g_rangeLow : g_rangeHigh)); return true; } //-------------------------------------------------------------------- // Chart objects //-------------------------------------------------------------------- void UpdateRangeObjects() { if(!InpDrawRangeObjects) { ObjectDelete(0, g_objHigh); ObjectDelete(0, g_objLow); ObjectDelete(0, g_objLabel); return; } if(!g_rangeValid) return; if(ObjectFind(0, g_objHigh) < 0) ObjectCreate(0, g_objHigh, OBJ_HLINE, 0, 0, g_rangeHigh); ObjectSetDouble(0, g_objHigh, OBJPROP_PRICE, g_rangeHigh); ObjectSetInteger(0, g_objHigh, OBJPROP_COLOR, InpColorRangeHigh); ObjectSetInteger(0, g_objHigh, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, g_objHigh, OBJPROP_WIDTH, 1); ObjectSetString (0, g_objHigh, OBJPROP_TEXT, "LT Range High (liq)"); if(ObjectFind(0, g_objLow) < 0) ObjectCreate(0, g_objLow, OBJ_HLINE, 0, 0, g_rangeLow); ObjectSetDouble(0, g_objLow, OBJPROP_PRICE, g_rangeLow); ObjectSetInteger(0, g_objLow, OBJPROP_COLOR, InpColorRangeLow); ObjectSetInteger(0, g_objLow, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, g_objLow, OBJPROP_WIDTH, 1); ObjectSetString (0, g_objLow, OBJPROP_TEXT, "LT Range Low (liq)"); string lex = ""; if(g_bias == BIAS_SELL) lex = " BI+ST"; else if(g_bias == BIAS_BUY) lex = " SI+BT"; string scalp = SoftScalpStance() ? " | ScalpStance" : ""; string st = "LT | " + EnumToString(g_state) + " | " + EnumToString(g_bias) + lex + " | H=" + DoubleToString(g_rangeHigh, _Digits) + " L=" + DoubleToString(g_rangeLow, _Digits) + scalp; if(ObjectFind(0, g_objLabel) < 0) ObjectCreate(0, g_objLabel, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, g_objLabel, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_objLabel, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, g_objLabel, OBJPROP_YDISTANCE, 20); ObjectSetString (0, g_objLabel, OBJPROP_TEXT, st); ObjectSetInteger(0, g_objLabel, OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, g_objLabel, OBJPROP_FONTSIZE, 9); } void ResetSetup(string reason) { Print("LiquidityTrapEA: reset — ", reason); g_bias = BIAS_NONE; g_state = ST_IDLE; g_sweptLevel = 0.0; g_sweepTime = 0; g_entryMode = ""; // Keep last range until rebuilt; mark for refresh g_rangeValid = false; } //==================================================================== // EVENT HANDLERS //==================================================================== int OnInit() { if(!SymbolAllowed()) { Print("LiquidityTrapEA: symbol ", _Symbol, " not enabled by inputs."); return INIT_PARAMETERS_INCORRECT; } g_trade.SetExpertMagicNumber(InpMagicNumber); g_trade.SetDeviationInPoints(InpSlippage); g_trade.SetTypeFillingBySymbol(_Symbol); g_state = ST_IDLE; g_bias = BIAS_NONE; g_lastM5Bar = 0; g_lastM30Bar = 0; // Warm start: if already in a position for this magic/symbol, park in trade state if(HasOpenPosition()) g_state = ST_IN_TRADE; Print("LiquidityTrapEA v1.20 initialized on ", _Symbol, " | lots XAU=", InpLotXAU, " BTC=", InpLotBTC, " default=", InpLotDefault, " RR=", InpRiskReward, " DirectInduce=", InpAllowDirectInduceEntry, " PreferQuickScalp=", InpPreferQuickScalp); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { ObjectDelete(0, g_objHigh); ObjectDelete(0, g_objLow); ObjectDelete(0, g_objLabel); } void OnTick() { // Always track open-position lifecycle if(g_state == ST_IN_TRADE) { if(!HasOpenPosition()) ResetSetup("position closed"); else { UpdateRangeObjects(); return; } } if(HasOpenPosition()) { g_state = ST_IN_TRADE; return; } // New-bar gate on M5 — avoid spam / re-entries mid-bar bool newM5 = IsNewBar(PERIOD_M5, g_lastM5Bar); bool newM30 = IsNewBar(PERIOD_M30, g_lastM30Bar); // Rebuild range on new M30 bar or when idle / invalid if(newM30 || g_state == ST_IDLE || !g_rangeValid) { if(BuildM30Range()) { if(g_state == ST_IDLE) g_state = ST_WAIT_SWEEP; UpdateRangeObjects(); } else if(g_state == ST_IDLE) { UpdateRangeObjects(); return; } } if(!g_rangeValid) return; //--- WAIT_SWEEP (HARD GATE): no bias / no trade until high OR low TAKEN. // Respect of high (approach without pierce) → stay out; do not short. if(g_state == ST_WAIT_SWEEP) { ENUM_BIAS b = BIAS_NONE; double lvl = 0.0; datetime t = 0; if(DetectFirstSweep(b, lvl, t)) { g_bias = b; g_sweptLevel = lvl; g_sweepTime = t; g_state = ST_WAIT_CHOCH; string tag = (b == BIAS_SELL ? "BI+ST" : "SI+BT"); Print("LiquidityTrapEA: extreme TAKEN → ", tag, " bias=", EnumToString(b), " level=", lvl, " time=", TimeToString(t), SoftScalpStance() ? " [soft scalp stance: HTF range wide]" : ""); UpdateRangeObjects(); } return; // CHOCH evaluated on subsequent M5 closes } //--- WAIT_CHOCH: only evaluate on new M5 bar (closed candle confirmation) if(g_state == ST_WAIT_CHOCH) { if(!newM5) { UpdateRangeObjects(); return; } // Invalidate if opposite extreme swept hard before we enter (range broken both ways) MqlRates m5[]; if(CopyRatesSafe(PERIOD_M5, 3, m5) >= 2) { if(g_bias == BIAS_SELL && m5[1].low < g_rangeLow) { // Already targeting low after high sweep — OK to continue toward 2R; // only reset if we never got CHOCH and price reclaimed far above (optional). // v1: if opposite liquidity also fully taken AND price closes back through // swept level the other way for several bars, rebuild. Soft invalidate: if(m5[1].close > g_rangeHigh) { ResetSetup("failed trap — close back above range high"); UpdateRangeObjects(); return; } } if(g_bias == BIAS_BUY && m5[1].high > g_rangeHigh) { if(m5[1].close < g_rangeLow) { ResetSetup("failed trap — close back below range low"); UpdateRangeObjects(); return; } } } // Prefer CHOCH (default path). Optionally allow direct induce wick-reject. bool entered = false; if(DetectChoCH(g_bias, g_sweepTime)) { g_entryMode = "CHOCH"; Print("LiquidityTrapEA: M5 CHOCH confirmed for ", EnumToString(g_bias), (g_bias == BIAS_SELL ? " BI+ST" : " SI+BT"), " (trap after induce / BOS LQ grab)", SoftScalpStance() ? " [soft scalp stance]" : ""); if(OpenTrade(g_bias)) { g_state = ST_IN_TRADE; entered = true; } else Print("LiquidityTrapEA: CHOCH ok but entry skipped (spread/stops/trade)."); } if(!entered && InpAllowDirectInduceEntry && DetectDirectInduceRejection(g_bias)) { g_entryMode = "DirectInduce"; Print("LiquidityTrapEA: direct induce reject for ", EnumToString(g_bias), " (wick into M30 extreme, close back inside — no CHOCH)"); if(OpenTrade(g_bias)) g_state = ST_IN_TRADE; else Print("LiquidityTrapEA: DirectInduce ok but entry skipped (spread/stops/trade)."); } UpdateRangeObjects(); return; } UpdateRangeObjects(); } //+------------------------------------------------------------------+