Above every obvious swing high sit buy stops, and below every swing low sit sell stops. Price often runs those pools of liquidity and then turns: the classic stop-run or liquidity sweep. The problem is that many sweeps are not reversals at all, just the start of a breakout.
The Liquidity Sweep & Absorption Reversal Engine by PhenLabs is built to separate the two. It tracks the pools of resting liquidity at recent swing highs and lows, detects the bar that sweeps one of them, and only flags a reversal when that bar also shows absorption: heavy volume traded with little price progress, the footprint of large players taking the other side. Each confirmed signal comes with a projected target.
The engine works in four steps:
A bullish signal comes from a swept SSL, a bearish one from a swept BSL. The risk is the larger of the ATR and the wick beyond the level, and the target is the nearest pool on the opposite side or, if there is none or the R-multiple mode is chosen, the close plus or minus the risk times the R-multiple.
On instruments without volume data the absorption filter never confirms: switch it off (useAbs = 0) to work with the sweeps alone.
//---------------------------------------------------------------
// PRC_LSA Reversal Engine
// version = 0
// 28.10.2026
// Iván González @ www.prorealcode.com
// Author: PhenLabs
// Sharing ProRealTime knowledge
//--------------------------------------------------------------------//
DEFPARAM DRAWONLASTBARONLY = true
// ===================== STRUCTURE & LIQUIDITY =====================
pivotLen = 8 // pivot lookback (bars on each side, min 2)
maxPools = 6 // max active pools per side (2 to 20)
sweepMode = 0 // sweep trigger: 0 = close back inside, 1 = displacement break
dispMult = 1.2 // displacement (x ATR), only used when sweepMode = 1
// ===================== ABSORPTION FILTER =====================
useAbs = 1 // 1 = require absorption confirmation
volMult = 1.6 // volume spike (x average volume)
bodyThresh = 0.45 // max body / range (lower = stricter absorption)
volLen = 20 // volume SMA length
// ===================== TARGETS & PROJECTION =====================
rMult = 2.0 // target R-multiple
showProj = 1 // 1 = show the projected draw
projStyle = 0 // target type: 0 = opposing pool, 1 = R-multiple
// ===================== VISUALS =====================
showDash = 1 // 1 = show the dashboard
// bullish signal / BSL line: 38,166,154 - bearish signal / SSL line: 239,83,80
prd = max(2, round(pivotLen))
mxP = min(20, max(2, round(maxPools)))
stride = 21
// ===================== CORE CALCULATIONS =====================
atrOk = 0
IF barindex >= 13 THEN
atrOk = 1
atrV = averagetruerange[14](close)
ENDIF
volAvg = average[volLen](volume)
volSpike = 0
IF barindex >= volLen - 1 AND volAvg > 0 AND volume >= volAvg * volMult THEN
volSpike = 1
ENDIF
rng = high - low
IF rng > 0 THEN
bodyR = abs(close - open) / rng
ELSE
bodyR = 0
ENDIF
absorbed = 0
IF volSpike = 1 AND bodyR <= bodyThresh THEN
absorbed = 1
ENDIF
// ===================== PIVOTS =====================
// confirmed prd bars later: strictly above the prd bars after it,
// at least as high as the prd bars before it
hhA = highest[prd](high)
llA = lowest[prd](low)
phOk = 0
plOk = 0
IF barindex >= 2 * prd THEN
IF high[prd] > hhA AND high[prd] >= hhA[prd + 1] THEN
phOk = 1
phV = high[prd]
ENDIF
IF low[prd] < llA AND low[prd] <= llA[prd + 1] THEN
plOk = 1
plV = low[prd]
ENDIF
ENDIF
// ===================== LIQUIDITY POOL STORE =====================
// Two buffers that alternate bar by bar: this bar starts from a copy of the
// pools as they were when the previous bar closed. Arrays are NOT rewound
// between ticks the way scalars are, so every tick of the live bar rebuilds
// its own copy and the confirmed pools are never touched twice.
pb = barindex MOD 2
qb = 1 - pb
ob = pb * stride
oq = qb * stride
IF barindex = 0 THEN
nB = 0
nS = 0
ENDIF
IF nB > 0 THEN
FOR i = 0 TO nB - 1 DO
$bL[ob + i] = $bL[oq + i]
NEXT
ENDIF
IF nS > 0 THEN
FOR i = 0 TO nS - 1 DO
$sL[ob + i] = $sL[oq + i]
NEXT
ENDIF
// new pivots join their pool; the oldest one leaves when the pool is full
IF phOk = 1 THEN
$bL[ob + nB] = phV
nB = nB + 1
IF nB > mxP THEN
FOR i = 0 TO nB - 2 DO
$bL[ob + i] = $bL[ob + i + 1]
NEXT
nB = nB - 1
ENDIF
ENDIF
IF plOk = 1 THEN
$sL[ob + nS] = plV
nS = nS + 1
IF nS > mxP THEN
FOR i = 0 TO nS - 2 DO
$sL[ob + i] = $sL[ob + i + 1]
NEXT
nS = nS - 1
ENDIF
ENDIF
// nearest active BSL above the low, nearest SSL below the high
bslIdx = -1
bslV = 0
IF nB > 0 THEN
FOR i = nB - 1 DOWNTO 0 DO
vv = $bL[ob + i]
IF low < vv AND (bslIdx = -1 OR vv < bslV) THEN
bslV = vv
bslIdx = i
ENDIF
NEXT
ENDIF
sslIdx = -1
sslV = 0
IF nS > 0 THEN
FOR i = nS - 1 DOWNTO 0 DO
vv = $sL[ob + i]
IF high > vv AND (sslIdx = -1 OR vv > sslV) THEN
sslV = vv
sslIdx = i
ENDIF
NEXT
ENDIF
// ===================== SWEEP ENGINE =====================
// bearish reversal: BSL swept (wick above) then rejected / absorbed
// bullish reversal: SSL swept (wick below) then rejected / absorbed
bslSwept = 0
IF bslIdx >= 0 AND high > bslV THEN
bslSwept = 1
ENDIF
sslSwept = 0
IF sslIdx >= 0 AND low < sslV THEN
sslSwept = 1
ENDIF
bslTrig = 0
sslTrig = 0
IF sweepMode = 0 THEN
IF bslSwept = 1 AND close < bslV THEN
bslTrig = 1
ENDIF
IF sslSwept = 1 AND close > sslV THEN
sslTrig = 1
ENDIF
ELSE
IF bslSwept = 1 AND atrOk = 1 AND close > bslV + atrV * dispMult THEN
bslTrig = 1
ENDIF
IF sslSwept = 1 AND atrOk = 1 AND close < sslV - atrV * dispMult THEN
sslTrig = 1
ENDIF
ENDIF
bslConfirm = 0
IF bslTrig = 1 AND (useAbs = 0 OR absorbed = 1) THEN
bslConfirm = 1
ENDIF
sslConfirm = 0
IF sslTrig = 1 AND (useAbs = 0 OR absorbed = 1) THEN
sslConfirm = 1
ENDIF
// swept pools are removed so they do not fire again
IF bslSwept = 1 THEN
IF bslIdx <= nB - 2 THEN
FOR i = bslIdx TO nB - 2 DO
$bL[ob + i] = $bL[ob + i + 1]
NEXT
ENDIF
nB = nB - 1
ENDIF
IF sslSwept = 1 THEN
IF sslIdx <= nS - 2 THEN
FOR i = sslIdx TO nS - 2 DO
$sL[ob + i] = $sL[ob + i + 1]
NEXT
ENDIF
nS = nS - 1
ENDIF
// ===================== SIGNALS AND PROJECTED DRAW =====================
// stored with a pure assignment indexed by a scalar counter (the counter
// rewinds between ticks, so a signal that disappears on the live bar leaves
// nothing behind) and redrawn on the last bar
IF atrOk = 1 THEN
sigOff = atrV
ELSE
sigOff = rng
ENDIF
once nSig = 0
once nPrj = 0
IF sslConfirm = 1 THEN
$sgX[nSig] = barindex
$sgY1[nSig] = low - 0.5 * sigOff
$sgY2[nSig] = low - 1.2 * sigOff
$sgY3[nSig] = low - 1.8 * sigOff
$sgD[nSig] = 1
nSig = nSig + 1
IF showProj = 1 AND atrOk = 1 THEN
risk = max(atrV, sslV - low)
IF projStyle = 0 AND bslIdx >= 0 THEN
tgt = bslV
ELSE
tgt = close + risk * rMult
ENDIF
$pjX[nPrj] = barindex
$pjY1[nPrj] = sslV
$pjY2[nPrj] = tgt
$pjD[nPrj] = 1
nPrj = nPrj + 1
ENDIF
ENDIF
IF bslConfirm = 1 THEN
$sgX[nSig] = barindex
$sgY1[nSig] = high + 0.5 * sigOff
$sgY2[nSig] = high + 1.8 * sigOff
$sgY3[nSig] = high + 1.2 * sigOff
$sgD[nSig] = -1
nSig = nSig + 1
IF showProj = 1 AND atrOk = 1 THEN
risk = max(atrV, high - bslV)
IF projStyle = 0 AND sslIdx >= 0 THEN
tgt = sslV
ELSE
tgt = close - risk * rMult
ENDIF
$pjX[nPrj] = barindex
$pjY1[nPrj] = bslV
$pjY2[nPrj] = tgt
$pjD[nPrj] = -1
nPrj = nPrj + 1
ENDIF
ENDIF
// ===================== DRAWING ON THE LAST BAR =====================
IF islastbarupdate THEN
tk = ticksize
IF tk <= 0 THEN
tk = 0.01
ENDIF
// active pools
IF nB > 0 THEN
FOR i = nB - 1 DOWNTO 0 DO
vv = $bL[ob + i]
DRAWSEGMENT(barindex - 60, vv, barindex + 8, vv) COLOURED(38, 166, 154, 255) STYLE(dottedline2, 1)
NEXT
ENDIF
IF nS > 0 THEN
FOR i = nS - 1 DOWNTO 0 DO
vv = $sL[ob + i]
DRAWSEGMENT(barindex - 60, vv, barindex + 8, vv) COLOURED(239, 83, 80, 255) STYLE(dottedline2, 1)
NEXT
ENDIF
// signals: triangle in the signal colour, "Bull / Sweep" text in blue
IF nSig > 0 THEN
FOR k = 0 TO nSig - 1 DO
sx = $sgX[k]
sy1 = $sgY1[k]
sy2 = $sgY2[k]
sy3 = $sgY3[k]
IF $sgD[k] = 1 THEN
DRAWTEXT("▲", sx, sy1, dialog, bold, 14) COLOURED(38, 166, 154, 255)
DRAWTEXT("Bull", sx, sy2, dialog, standard, 10) COLOURED(41, 98, 255, 255)
DRAWTEXT("Sweep", sx, sy3, dialog, standard, 10) COLOURED(41, 98, 255, 255)
ELSE
DRAWTEXT("▼", sx, sy1, dialog, bold, 14) COLOURED(239, 83, 80, 255)
DRAWTEXT("Bear", sx, sy2, dialog, standard, 10) COLOURED(41, 98, 255, 255)
DRAWTEXT("Sweep", sx, sy3, dialog, standard, 10) COLOURED(41, 98, 255, 255)
ENDIF
NEXT
ENDIF
// projected draws: the original keeps its 500 most recent lines (pool lines
// included) and its 500 most recent labels
firstPrj = max(0, nPrj - (500 - nB - nS))
IF nPrj > 0 THEN
FOR k = firstPrj TO nPrj - 1 DO
px = $pjX[k]
py1 = $pjY1[k]
py2 = $pjY2[k]
tgtP = round(py2 / tk) * tk
IF $pjD[k] = 1 THEN
DRAWSEGMENT(px, py1, px + 20, py2) COLOURED(38, 166, 154, 255) STYLE(dottedline, 2)
DRAWTEXT("#tgtP#", px + 24, py2, dialog, bold, 10) COLOURED(38, 166, 154, 255)
ELSE
DRAWSEGMENT(px, py1, px + 20, py2) COLOURED(239, 83, 80, 255) STYLE(dottedline, 2)
DRAWTEXT("#tgtP#", px + 24, py2, dialog, bold, 10) COLOURED(239, 83, 80, 255)
ENDIF
NEXT
ENDIF
// ---------- dashboard (top right) ----------
IF showDash = 1 THEN
pz = 10
chW = pz * 0.62
rowH = pz + 8
padX = 6
col0W = 12 * chW + 2 * padX
col1W = 19 * chW + 2 * padX
pW = col0W + col1W
pH = 6 * rowH
pL = 0 - 85 - pW
pT = 0 - 12
xC0 = pL + col0W / 2
xC1 = pL + col0W + col1W / 2
y0 = pT - rowH * 0.5
y1 = pT - rowH * 1.5
y2 = pT - rowH * 2.5
y3 = pT - rowH * 3.5
y4 = pT - rowH * 4.5
y5 = pT - rowH * 5.5
bslP = round(bslV / tk) * tk
sslP = round(sslV / tk) * tk
// opaque background: a transparent dark table turns grey on a light chart
DRAWRECTANGLE(pL, pT, pL + pW, pT - pH) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(48, 54, 61, 255) FILLCOLOR(13, 17, 23, 255)
DRAWTEXT("LSA Engine", xC0, y0, monospaced, bold, 11) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(201, 209, 217, 255)
DRAWTEXT("PhenLabs", xC1, y0, monospaced, bold, 11) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(126, 138, 154, 255)
DRAWTEXT("Nearest BSL", xC0, y1, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
DRAWTEXT("Nearest SSL", xC0, y2, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
DRAWTEXT("Vol Spike", xC0, y3, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
DRAWTEXT("Absorption", xC0, y4, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
DRAWTEXT("Mode", xC0, y5, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
IF bslIdx >= 0 THEN
DRAWTEXT("#bslP#", xC1, y1, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(38, 166, 154, 255)
ELSE
DRAWTEXT("—", xC1, y1, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(38, 166, 154, 255)
ENDIF
IF sslIdx >= 0 THEN
DRAWTEXT("#sslP#", xC1, y2, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(239, 83, 80, 255)
ELSE
DRAWTEXT("—", xC1, y2, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(239, 83, 80, 255)
ENDIF
IF volSpike = 1 THEN
DRAWTEXT("YES", xC1, y3, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(241, 196, 15, 255)
ELSE
DRAWTEXT("no", xC1, y3, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
ENDIF
IF absorbed = 1 THEN
DRAWTEXT("YES", xC1, y4, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(241, 196, 15, 255)
ELSE
DRAWTEXT("no", xC1, y4, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(139, 148, 158, 255)
ENDIF
IF sweepMode = 0 THEN
DRAWTEXT("Close Back Inside", xC1, y5, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(201, 209, 217, 255)
ELSE
DRAWTEXT("Displacement Break", xC1, y5, monospaced, bold, pz) ANCHOR(TOPRIGHT, XSHIFT, YSHIFT) COLOURED(201, 209, 217, 255)
ENDIF
ENDIF
ENDIF
RETURN