984 lines
33 KiB
MQL5
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();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
|