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();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|