Liquidation sweep reversal
Fade a sweep into a dense cluster of the liquidation model once real liquidations spike, open interest falls and the delta turns, with the stop beyond the sweep.
The liquidation heatmap is a model, not a list of orders: from open interest and taker flow it estimates where positions were opened, at which leverage, and so where they would be liquidated. Dense clusters of those levels act as magnets. Price runs into them, the forced orders fire in a burst, and once the burst is spent and the positions are gone the move often turns. This strategy waits for all of that to be visible in the data before it fades the sweep, and puts its stop beyond the sweep’s extreme.
It ships with Flowscope as a community script. It reads the cluster from
data.liqmap, the
liquidations from data.stat, open interest from data.oi and the delta from the
candles.
The script
script "Liquidation Sweep Reversal"
// Fades a sweep into a dense liquidation cluster once the forced flow is
// spent: price runs into the levels where the liquidation model expects
// many positions to be liquidated (a "magnet"), liquidations really fire,
// open interest falls (the positions are gone) and the delta turns.
//
// Long (a sweep down; shorts mirror it):
// 1. Cluster: on the bar before, the model's long-liquidation levels
// within the search range below the price hold a cell of at least
// the set multiple of their average cell (and the smallest size).
// 2. Sweep: this bar's low trades into the middle of that cell.
// 3. Flush: the longs liquidated in the bar reach the set multiple of
// their average and the smallest size; open interest falls by at
// least the set share over the bar.
// 4. Turn: the bar closes green with buying delta, or one of the next
// bars does; new lows meanwhile move the sweep's low, unless price runs
// on through the cluster (more than the set ATRs past it).
// Entry at the next open; stop beyond the sweep low (a buffer of ATR) or
// a fixed ATR distance; target a multiple of the risk or the nearest
// short-liquidation cluster above; a time stop after the set bars.
//
// Everything is read from completed bars: the cluster from the map of the
// bar before the sweep, the liquidations, open interest and delta of the
// bar that just closed. Orders fill at the next bar's open.
input (
input.tab(title: "Settings")
input.section(title: "Market")
marketSel = input.select("Chart market", title: "Market", options: ["Chart market", "Binance + Bybit + OKX", "Binance", "Bybit", "OKX"], description: "Whose liquidation map, liquidations, open interest and delta are read. Chart market (default): the chart's own. Binance + Bybit + OKX: the three modeled venues summed (the chart's coin there). The model covers Binance, Bybit and OKX futures; spot and other venues read nothing.")
levFrom = input.select("5x", title: "Leverage from", options: ["5x", "10x", "25x", "50x", "75x", "100x"], description: "The lowest leverage tier the map counts. The model spreads every position it estimates over 5x, 10x, 25x, 50x, 75x and 100x; high tiers sit close to the entry, low tiers far from it.")
levTo = input.select("100x", title: "Leverage to", options: ["5x", "10x", "25x", "50x", "75x", "100x"], sameLine: true, description: "The highest leverage tier the map counts.")
input.section(title: "Cluster")
searchPct = input.float(1.5, title: "Search range (%)", min: 0.1, max: 10.0, description: "How far from the price a cluster may sit, in percent of the price.")
clusterMult = input.float(3.0, title: "Density (x average cell)", min: 1.0, max: 50.0, description: "A cluster's cell must hold at least this multiple of the average cell in the search range: a magnet, not the background.")
minClusterUsd = input.float(250000.0, title: "Smallest cluster (USD)", min: 0.0, description: "And at least this notional. 0 turns the floor off.")
input.section(title: "Flush and turn")
liqLen = input.int(60, title: "Liquidation average (bars)", min: 5, max: 1000, description: "The bars the liquidations of the sweep are compared with.")
liqMult = input.float(3.0, title: "Liquidation spike (x average)", min: 1.0, max: 100.0, description: "The liquidations of the swept side in the sweep bar against their average.")
minLiqUsd = input.float(100000.0, title: "Smallest flush (USD)", min: 0.0, description: "And at least this much liquidated in the bar.")
oiDropPct = input.float(0.05, title: "Open interest drop (%)", min: 0.0, max: 10.0, description: "Open interest must fall by at least this share over the sweep bar: positions closed, not opened. Bars without open interest give no signal.")
confirmBars = input.int(5, title: "Bars for the turn", min: 1, max: 30, description: "How many bars after the sweep the delta may take to turn. 1: the sweep bar itself must close as the turn.")
maxBeyond = input.float(1.0, title: "Run past the cluster (ATR)", min: 0.0, max: 10.0, description: "While waiting for the turn, price may push further (the stop follows the new extreme), but not more than this beyond the cluster: then the cascade goes on and the setup is dropped.")
input.section(title: "Exits")
stopMode = input.select("Beyond the sweep", title: "Stop", options: ["Beyond the sweep", "ATR"], description: "Beyond the sweep: below the sweep's low (above its high for shorts) by the buffer. ATR: a fixed distance from the entry.")
stopBuffer = input.float(0.25, title: "Buffer beyond the sweep (ATR)", min: 0.0, max: 5.0)
stopAtr = input.float(1.5, title: "ATR stop distance", min: 0.1, max: 20.0, sameLine: true)
targetMode = input.select("R multiple", title: "Target", options: ["R multiple", "Opposite cluster"], description: "R multiple: the risk times the multiple. Opposite cluster: the nearest cluster of the other side within the target range (the next magnet), else the R multiple; never less than 1 R.")
targetR = input.float(2.0, title: "Target (R)", min: 0.2, max: 20.0)
targetPct = input.float(3.0, title: "Opposite cluster range (%)", min: 0.1, max: 20.0, sameLine: true)
maxBars = input.int(60, title: "Time stop (bars)", min: 1, max: 5000, description: "A trade still open after this many bars is closed at market.")
input.section(title: "Risk")
riskPct = input.float(0.5, title: "Risk per trade (% equity)", min: 0.05, max: 10.0, description: "The size is set so that the stop loses this share of the equity.")
maxExposure = input.float(3.0, title: "Largest position (x equity)", min: 0.1, max: 50.0, description: "A tight stop never makes the position larger than this multiple of the equity.")
feePct = input.float(0.05, title: "Fee per side (%)", min: 0.0, max: 1.0, description: "Taker fee of each fill, for the Strategy Tester.")
allowLong = input.bool(true, title: "Longs")
allowShort = input.bool(true, title: "Shorts", sameLine: true)
showLevels = input.bool(true, title: "Show clusters, stops and targets")
longColor = input.color(#00e676, title: "Long")
shortColor = input.color(#ff1744, title: "Short", sameLine: true)
input.tab(title: "About")
input.section(title: "The idea", description: "The liquidation heatmap is a model, not real orders: from open interest and taker flow it estimates where positions were opened and at which leverage, and so where they would be liquidated. Dense clusters of those levels act as magnets: price is drawn to them, and when it gets there the forced orders fire in a burst. Once the burst is over and open interest has dropped, the move has lost its fuel and often turns.")
input.section(title: "What has to happen", description: "A cluster near the price on the bar before; the bar sweeping into it; real liquidations of that side spiking (data.stat) while open interest falls (data.oi); then the delta turning against the sweep. Only then a trade, against the sweep, with the stop beyond the sweep's extreme.")
input.section(title: "Reading the chart", description: "The dotted lines are the nearest clusters below and above the price. A diamond marks a sweep that flushed; a label marks the entry with the liquidations and the drop of open interest. Entry, stop and target lines run while the trade is open.")
input.section(title: "Data", description: "data.liqmap reads the same model as the Liquidation Heatmap, cell by cell, with the leverage tiers chosen here. Liquidations and open interest come from the market's recorded context, including the hub's history. Spot markets and venues without the model read nothing and give no signals.")
input.section(title: "Limits", description: "Few sweeps qualify: on BTC one minute bars a handful a day at the defaults. The model is an estimate; Hyperliquid's maps (map: liqmap.hlLiquidations) are built from public positions instead. Backtests over short histories are anecdotes, not evidence.")
)
setup (
venue = match marketSel {
"Binance + Bybit + OKX" => markets.agg("binancef", "bybitf", "okxf")
"Binance" => "binancef"
"Bybit" => "bybitf"
"OKX" => "okxf"
_ => context.exchange
}
levMin = match levFrom {
"10x" => 10.0
"25x" => 25.0
"50x" => 50.0
"75x" => 75.0
"100x" => 100.0
_ => 5.0
}
levMax = match levTo {
"5x" => 5.0
"10x" => 10.0
"25x" => 25.0
"50x" => 50.0
"75x" => 75.0
_ => 100.0
}
)
strategy(
initialCapital: 10000.0,
commissionType: strategy.commission.percent,
commissionValue: feePct,
slippage: 2,
leverage: 10.0,
fundingCosts: true
)
data (
chart = subscribe(data.ohlcv)
flow = subscribe(data.ohlcv, exchange: venue)
liq = subscribe(data.liqmap, exchange: venue, minLeverage: levMin, maxLeverage: levMax)
stats = subscribe(data.stat, exchange: venue)
oi = subscribe(data.oi, exchange: venue)
)
plot (
belowLine = plot.line(title: "Cluster below", color: #ff9800a0, style: linestyle.dotted, showValue: false)
aboveLine = plot.line(title: "Cluster above", color: #40c4ffa0, style: linestyle.dotted, showValue: false)
)
type Setup {
side: string
price: float
liquidated: float
oiChangePct: float
}
state (
// The sweep waiting for its turn: 1 long, -1 short, 0 none.
armed = 0
armedAt = 0
sweepExtreme = 0.0
clusterEdge = 0.0
sweepLiq = 0.0
sweepOi = 0.0
prevLongAvg: float? = null
prevShortAvg: float? = null
// The trade drawn.
tradeDir = 0
entryPrice = 0.0
stopPrice = 0.0
targetPrice = 0.0
tradeFrom: time? = null
nextId = 0
marks = entities.markerPool(max: 400)
labels = entities.labelPool(max: 400)
lines = entities.linePool(max: 1200)
)
alert longAlert = alert(title: "Liquidation sweep long", onClose: true, payload: Setup)
alert shortAlert = alert(title: "Liquidation sweep short", onClose: true, payload: Setup)
// The smallest cluster notional within `pct` below (or above) the price of
// the map `ago` bars back: the floor, and the density multiple of the
// average cell there.
fn clusterFloor(pct: float, up: bool, ago: int) -> float {
let ref = liq.price(barsAgo: ago) ?? 0.0
let cell = liq.cellSize(barsAgo: ago) ?? 0.0
if ref <= 0.0 || cell <= 0.0 {
return minClusterUsd
}
let total = up ? liq.densityAbove(pct, barsAgo: ago) ?? 0.0 : liq.densityBelow(pct, barsAgo: ago) ?? 0.0
let cells = math.max(1.0, ref * pct / 100.0 / cell)
return math.max(minClusterUsd, clusterMult * total / cells)
}
// Draws the open trade's lines up to `t`.
fn drawTrade(t: time) {
if !showLevels || tradeFrom == null {
return
}
let start = tradeFrom ?? t
lines.get(nextId * 3).set(start, entryPrice, t, entryPrice, width: 1.0, color: #ffffffb0, style: linestyle.dashed, text: str.format("Entry {0:.2}", entryPrice))
lines.get(nextId * 3 + 1).set(start, stopPrice, t, stopPrice, width: 1.0, color: #ff5252, text: str.format("SL {0:.2}", stopPrice))
lines.get(nextId * 3 + 2).set(start, targetPrice, t, targetPrice, width: 1.0, color: #26a69a, text: str.format("TP {0:.2}", targetPrice))
}
on chart.close {
let o = chart.open
let h = chart.high
let l = chart.low
let c = chart.close
let t = chart.time
// Stateful helpers on every bar.
let atr = chart.atr(14)
let longLiq = stats.sellLiq ?? 0.0
let shortLiq = stats.buyLiq ?? 0.0
let longAvgNow = ta.sma(longLiq, liqLen)
let shortAvgNow = ta.sma(shortLiq, liqLen)
let longAvg = prevLongAvg
let shortAvg = prevShortAvg
prevLongAvg = longAvgNow
prevShortAvg = shortAvgNow
if o == null || h == null || l == null || c == null || t == null || atr == null || atr <= 0.0 {
return
}
// The magnets: the nearest dense clusters of the map just completed.
let below = liq.clusterBelow(searchPct, minSize: clusterFloor(searchPct, false, 0))
let above = liq.clusterAbove(searchPct, minSize: clusterFloor(searchPct, true, 0))
if showLevels {
belowLine.plot(below == null ? null : below.price + below.binSize / 2.0)
aboveLine.plot(above == null ? null : above.price + above.binSize / 2.0)
}
// The trade on the chart ends with the position.
let flat = strategy.positionSize == 0.0
if tradeDir != 0 {
drawTrade(t)
if flat {
tradeDir = 0
}
}
// Time stop.
let open = strategy.openTrade(0)
if open != null && chart.index - open.entryBarIndex >= maxBars {
strategy.closeAll(comment: "Time stop")
}
let delta = (flow.buyVolume ?? 0.0) - (flow.sellVolume ?? 0.0)
let oiOpen = oi.open
let oiClose = oi.close
let oiChange = oiOpen != null && oiClose != null && oiOpen > 0.0 ? (oiClose / oiOpen - 1.0) * 100.0 : 0.0
let oiKnown = oiOpen != null && oiClose != null && oiOpen > 0.0
// A new sweep: into the cluster of the bar before, flushing that side.
if flat && armed == 0 && oiKnown && oiChange <= 0.0 - oiDropPct {
let prevBelow = liq.clusterBelow(searchPct, minSize: clusterFloor(searchPct, false, 1), barsAgo: 1)
let prevAbove = liq.clusterAbove(searchPct, minSize: clusterFloor(searchPct, true, 1), barsAgo: 1)
let longFlush = longAvg != null && longLiq >= minLiqUsd && longLiq >= liqMult * longAvg
let shortFlush = shortAvg != null && shortLiq >= minLiqUsd && shortLiq >= liqMult * shortAvg
if allowLong && prevBelow != null && longFlush && l <= prevBelow.price + prevBelow.binSize / 2.0 {
armed = 1
armedAt = chart.index
sweepExtreme = l
clusterEdge = prevBelow.price
sweepLiq = longLiq
sweepOi = oiChange
marks.get(chart.index).set(t, l - 0.2 * atr, shape: shape.diamond, color: longColor, size: 7.0)
} else if allowShort && prevAbove != null && shortFlush && h >= prevAbove.price + prevAbove.binSize / 2.0 {
armed = -1
armedAt = chart.index
sweepExtreme = h
clusterEdge = prevAbove.top
sweepLiq = shortLiq
sweepOi = oiChange
marks.get(chart.index).set(t, h + 0.2 * atr, shape: shape.diamond, color: shortColor, size: 7.0)
}
}
if !flat || armed == 0 {
return
}
// Price pushing on: the sweep's extreme (and so the stop) follows it,
// unless it ran through the cluster. No turn in time: the setup is over.
let ranThrough = armed > 0 ? l < clusterEdge - maxBeyond * atr : h > clusterEdge + maxBeyond * atr
if ranThrough || chart.index - armedAt >= confirmBars {
armed = 0
return
}
sweepExtreme = armed > 0 ? math.min(sweepExtreme, l) : math.max(sweepExtreme, h)
let turned = armed > 0 ? c > o && delta > 0.0 : c < o && delta < 0.0
if !turned {
return
}
// The trade: sized by the risk to the stop, capped by the exposure.
let dir = armed
armed = 0
let sign = dir > 0 ? 1.0 : -1.0
let stop = stopMode == "ATR" ? c - sign * stopAtr * atr : sweepExtreme - sign * stopBuffer * atr
let risk = (c - stop) * sign
if risk <= 0.0 {
return
}
let rTarget = c + sign * targetR * risk
let target = rTarget
if targetMode == "Opposite cluster" {
let next = dir > 0 ? liq.clusterAbove(targetPct, minSize: clusterFloor(targetPct, true, 0)) : liq.clusterBelow(targetPct, minSize: clusterFloor(targetPct, false, 0))
if next != null {
// Just before the magnet, at least one R away.
let magnet = dir > 0 ? next.price : next.top
target = (magnet - c) * sign >= risk ? magnet : rTarget
}
}
let qty = math.min(strategy.equity * riskPct / 100.0 / risk, strategy.equity * maxExposure / c)
if qty <= 0.0 {
return
}
let name = dir > 0 ? "Sweep long" : "Sweep short"
strategy.entry(name, dir > 0 ? strategy.long : strategy.short, qty: qty, comment: str.format("liq {0:,.0} · OI {1:.2}%", sweepLiq, sweepOi))
strategy.exit(name + " exit", name, stop: stop, limit: target, comment: "SL / TP")
nextId += 1
tradeDir = dir
entryPrice = c
stopPrice = stop
targetPrice = target
tradeFrom = t
drawTrade(t)
let text = str.format("{0}\nliq {1:,.0}\nOI {2:.2}%", dir > 0 ? "Long" : "Short", sweepLiq, sweepOi)
labels.get(nextId).set(t, dir > 0 ? l - 0.6 * atr : h + 0.6 * atr, text, color: dir > 0 ? longColor : shortColor)
let payload = Setup { side: dir > 0 ? "long" : "short", price: c, liquidated: sweepLiq, oiChangePct: sweepOi }
if dir > 0 {
longAlert.trigger(payload)
} else {
shortAlert.trigger(payload)
}
}
on chart.update {
let status = strategy.positionSize > 0.0 ? " · long" : strategy.positionSize < 0.0 ? " · short" : armed != 0 ? " · swept, waiting for the turn" : ""
script.setTitle(str.format("Liquidation Sweep Reversal{0}", status))
}Open a Binance, Bybit or OKX perpetual (or their aggregate), 1m to 15m, and add the script. The Strategy Tester opens with the backtest.
How it works
The cluster comes from the bar before
liq.clusterBelow(searchPct, minSize: …, barsAgo: 1) finds the nearest heatmap cell
of long liquidations below the price as the map stood on the bar before the sweep.
Reading the bar of the sweep itself would see the cluster already half gone (the map
of a bar is its time-weighted mean, and swept levels leave it). The cell has to hold
clusterMult times the average cell within the search range and at least
minClusterUsd: a magnet, not the background. clusterFloor computes that threshold
from liq.densityBelow and liq.cellSize.
What makes it a flush
The bar must trade into the middle of that cell, and three things have to show in the same bar:
| Check | Read | Default |
|---|---|---|
| Real liquidations of the swept side spike | stats.sellLiq (longs) against its 60-bar average | 3× and at least $100,000 |
| Positions closed, not opened | oi.close / oi.open - 1 | at most −0.05 % |
| The sweep reached the cluster | the bar’s low against the cell’s middle | — |
Bars without open interest give no signal: missing data is not a drop.
The turn
The setup waits up to confirmBars bars for the delta to turn: a green bar with more
taker buying than selling (shorts: a red bar with net selling). If price pushes further
meanwhile, the sweep’s extreme moves with it, and with it the stop; if it runs more
than maxBeyond ATRs through the cluster, the cascade is still going and the setup is
dropped.
Exits and size
| Exit | Rule |
|---|---|
| Stop | Beyond the sweep’s extreme by stopBuffer ATR, or stopAtr ATR from the entry |
| Target | targetR times the risk, or the nearest cluster on the other side within targetPct (at least 1 R) |
| Time stop | strategy.closeAll after maxBars bars |
The size risks riskPct of the equity to the stop, capped at maxExposure times the
equity. Commission defaults to 0.05 % a side, slippage to 2 ticks, and funding is
charged while a position is open.
Leverage tiers
levFrom and levTo keep only some tiers of the model (minLeverage: and
maxLeverage: of the subscription). 50x to 100x keeps the clusters close to the
entries, the ones a quick sweep reaches; 5x to 25x the deeper ones. See
leverage tiers for how the split
is made.
No look-ahead
Every value comes from bars that have closed: the cluster from the bar before the sweep, the liquidations, open interest and delta from the bar that just closed. Orders fill at the next bar’s open. A backtest cut short at any bar trades exactly as the full one up to that bar (the test suite checks it).
What we measured
On the BTCUSDT Binance perpetual with the data hub’s history from Thursday 15 October 14:52 to Friday 16 October 19:15 UTC (about 28 hours, all the history there was), at the defaults:
| Chart | Trades | Result |
|---|---|---|
| 1m | 1 long (16 Oct 15:09, out at the target two minutes later) | +$49.67 on $10,000 (+0.50 %), commission $30.04 |
| 5m | 1 short (16 Oct 11:35, after $7.1M of shorts were liquidated) | +$79.89 (+0.80 %) |
Three sweeps flushed into a cluster on the 1m chart in those hours; one turned in time. One or two trades are an anecdote, not evidence: test it on the longest history you have before trusting any number.
Adapting it
- Aggregate.
Market: Binance + Bybit + OKXsums the three modeled venues: their clusters, liquidations and open interest. - Tighter clusters. Raise
clusterMultto 5 to trade only the densest magnets. - Hyperliquid. Subscribe to
liqmap.hlLiquidationsinstead of the model for the liquidation prices of Hyperliquid’s public positions. - Confirm with the book. Require a bid wall under the sweep with
book.wallBelow, as in wall absorption.
Related
- Order book and liquidation maps
- Liquidation magnets, the realised side
- Strategies & backtesting