salim/LiquidityTrapEA.mq5

984 lines
33 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Trade/Trade.mqh>
//====================================================================
// 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();
}
//+------------------------------------------------------------------+