FlowscopeDocs

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.

intrabar-order-flow.fs
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:

Flowscope Script
buyUsd += usd
buyPxUsd += tr.price * usd

This 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:

Flowscope Script
let longsTrapped = (buyAt ?? 0.0) >= 100.0 - edge && closeAt < 50.0 && buyUsd > sellUsd

Large 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 buyCount and sellCount to 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.volume and compare the large-print prices with the bar’s point of control from profile.poc().