Intrabar order flow
Find out where inside each bar the large trades landed and flag bars where big buyers or sellers ended up trapped.
A candle tells you where price went, not who pushed it there. Two bars with identical OHLC can hide opposite stories: in one, large buyers lifted the low and price followed; in the other, large buyers chased the high and the bar then collapsed underneath them.
This script folds large trades into a per-bar summary. When the bar closes, it knows the volume-weighted price of the large buys and of the large sells, and where those prices sit in the bar’s final range. It plots both positions in a pane from 0 (bar low) to 100 (bar high), labels busy bars with their large-print count and notional, and marks two patterns: large buying near the high of a bar that closed weak, and large selling near the low of a bar that closed strong. Those are the bars where the big side is likely trapped.
script "Intrabar order flow"
input (
minUsd = input.float(100_000.0, title: "Minimum print (USD)", min: 1_000.0)
labelUsd = input.float(5_000_000.0, title: "Label bars from (USD)", min: 10_000.0)
edge = input.float(33.0, title: "Edge zone (% of range)", min: 5.0, max: 50.0)
)
data (
chart = subscribe(data.ohlcv)
tape = subscribe(data.trades)
)
state (
buyUsd = 0.0
sellUsd = 0.0
buyPxUsd = 0.0
sellPxUsd = 0.0
prints = 0
labels = entities.labelPool(max: 300)
)
pane posPane = pane(title: "Large-print position in bar (0 = low, 100 = high)", height: 0.22)
plot (
buyPos = plot.point(title: "Large buys", color: color.green, size: 4.0, on: posPane)
sellPos = plot.point(title: "Large sells", color: color.red, size: 4.0, on: posPane)
mid = plot.line(title: "Mid", color: color.gray, style: linestyle.dotted, on: posPane)
trappedLongs = plot.marker(title: "Trapped buyers", color: color.red, shape: shape.down)
trappedShorts = plot.marker(title: "Trapped sellers", color: color.green, shape: shape.up)
)
on tape.trade(tr) {
let usd = tr.price * tr.size
if usd >= minUsd {
prints += 1
if tr.isBuy {
buyUsd += usd
buyPxUsd += tr.price * usd
} else {
sellUsd += usd
sellPxUsd += tr.price * usd
}
}
}
on chart.close {
let h = chart.high
let l = chart.low
let c = chart.close
let t = chart.time
mid.plot(50.0)
if h != null && l != null && c != null && t != null && h > l {
let range = h - l
// Volume-weighted price of each side's large prints, as % of the bar range.
let buyAt: float? = buyUsd > 0.0 ? (buyPxUsd / buyUsd - l) / range * 100.0 : null
let sellAt: float? = sellUsd > 0.0 ? (sellPxUsd / sellUsd - l) / range * 100.0 : null
let closeAt = (c - l) / range * 100.0
buyPos.plot(buyAt)
sellPos.plot(sellAt)
// Big buyers near the high, close in the lower half: buyers trapped.
let longsTrapped = (buyAt ?? 0.0) >= 100.0 - edge && closeAt < 50.0 && buyUsd > sellUsd
// Big sellers near the low, close in the upper half: sellers trapped.
let shortsTrapped = sellAt != null && sellAt <= edge && closeAt > 50.0 && sellUsd > buyUsd
trappedLongs.plot(longsTrapped ? h : null)
trappedShorts.plot(shortsTrapped ? l : null)
// Label busy bars with their large-print count and notional.
let total = buyUsd + sellUsd
if total >= labelUsd {
let text = str.format("{0} prints, {1:,.0} USD", prints, total)
labels.get(t).set(t, h, text, color: buyUsd >= sellUsd ? color.green : color.red)
}
}
buyUsd = 0.0
sellUsd = 0.0
buyPxUsd = 0.0
sellPxUsd = 0.0
prints = 0
}How it works
Accumulate sums, not lists
The trade handler does not store individual prints. For each side it keeps two running sums: the notional, and price times notional. Their ratio at the close is the volume-weighted average price of that side’s large prints:
buyUsd += usd
buyPxUsd += tr.price * usdThis keeps the work per trade constant and the state small, no matter how many prints a bar receives. The final range of the bar is only known when it closes, so the position calculation happens in on chart.close, after which the sums are reset.
Position in the range
(vwap - low) / range * 100 turns each side’s average price into a number from 0 to 100, with a dotted line at 50. When one side had no large prints in a bar, its value is null and no dot is drawn. The float? annotations type the null branch of each conditional.
Trapped participants
The two patterns are simple comparisons on the positions:
let longsTrapped = (buyAt ?? 0.0) >= 100.0 - edge && closeAt < 50.0 && buyUsd > sellUsdLarge buying dominated, it happened in the top edge percent of the range, and the bar still closed in its lower half. Someone bought size near the high and is now under water. The ?? 0.0 turns a missing value into a number that can never pass the test; the sell-side condition shows the alternative, an explicit sellAt != null that narrows the local.
Labels keyed by time
A pool key can be a time, so labels.get(t) gives every bar its own label without a counter.
History
On a chart, trades reach back as far as the chart keeps them in memory. Older bars have no prints, so their positions stay empty.
Adapting it
- Count instead of size. Keep
buyCountandsellCountto tell a single block apart from a burst of medium prints. - Combine with absorption. A trapped-buyer bar that also shows VD absorption is a strong sign that a passive seller was working the high.
- Confirm with the volume profile. Subscribe to
data.volumeand compare the large-print prices with the bar’s point of control fromprofile.poc().
Related
- Large prints marks the prints themselves.
- Data and update events
plotreference,entitiesreference