# 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.

```flowscope title="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
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
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](/docs/scripting/examples/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()`.

## Related

- [Large prints](/docs/scripting/examples/large-prints) marks the prints themselves.
- [Data and update events](/docs/scripting/guides/data-and-events#trades)
- [`plot` reference](/docs/scripting/reference/catalog/plot), [`entities` reference](/docs/scripting/reference/catalog/entities)
