> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xarchive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Trapped traders

> Most of a bar's new longs opened above its point of control and the bar closed below it, so they are underwater; shorts the other way round.

![Trapped traders on a BTC Bid × Ask footprint chart](https://mintcdn.com/0xarchive-e895b8e7/dci2Lrw-3r2OqL5u/images/terminal/examples/trapped-traders.webp?fit=max\&auto=format\&n=dci2Lrw-3r2OqL5u\&q=85\&s=44a5eb0cafdac55b5614a50f46d71494)

BTC on 15-minute bars in the 0xArchive Terminal, 2026-10-23.

> **[Open on a chart](https://terminal.0xarchive.io/?symbol=BTC\&interval=15m\&scripts=example%3Atrapped-traders)**
>
> Load it on BTC 15m in the terminal, then change its settings or open it in the script editor.

Traders who opened on the wrong side of a bar's busiest price: most of the bar's **new longs** opened wholly above its point of control, and the bar closed back below it, so they are underwater at the close. **New shorts** below the PoC on a bar that closed above it are the mirror. A footprint shows where takers bought; only each fill's `takerWhy` says which of them opened a position.

The rows where they opened are tinted, deeper where more opened, and outlined, leaving the footprint's numbers readable. A line at their average entry runs right until price trades back to it, where they can get out flat, or until the watch ends; while it stands it is tagged with how much is trapped.

The share is of the bar's own new positions, so a market where most buying opens longs (HYPE: three quarters of it, in October 2026) is not trapped on every bar. A second input asks for at least 5% of the bar's volume, so a handful of new positions is not a trap.

## How it works

`trades().onTrade` adds each opening fill's size to a [`tallyBy`](https://docs.0xarchive.io/terminal/guides/state#tallies) keyed by its row, `Math.floor(t.price / chart.rowSize)`, and side. On each closed bar, `profile.bar().poc()` at [`chart.rowSize`](https://docs.0xarchive.io/terminal/guides/order-flow#footprint-rows) gives the point of control on the footprint's rows, and the rows past it are summed. The entries are kept in `state.list`s and drawn on the newest bar, as [Large taker orders](https://docs.0xarchive.io/terminal/examples/large-takers.md) draws its levels.

## Try changing

* Count only longs opened in the bar's top third, the ones that bought the high.
* Tag the entry line with how many bars it has stood.

## The script

```ts trapped-traders.ts theme={"theme":"github-dark"}
// Trapped traders: most of a bar's new longs opened above its point of control, and the bar closed
// back below it, so they are underwater at the close (shorts the other way round). Each fill's
// `takerWhy` says who opened, which a footprint alone cannot. The share is of the bar's own new
// longs, so a market where most buying opens longs is not trapped on every bar. The rows where they
// opened are tinted, deeper where more opened, leaving the footprint's numbers readable; a line at
// their average entry runs right until price trades back to it, where they get out flat, or until
// the watch ends, tagged with how much is trapped while it stands.
indicator('Trapped traders', { overlay: true, shortName: 'Trapped', maxLines: 100, maxLabels: 20 });

const side = input.choice('Side', 'both', ['longs', 'shorts', 'both'] as const, { labels: { longs: 'Trapped longs', shorts: 'Trapped shorts', both: 'Both' } });
const minShare = input.float('Share of the bar\'s new positions trapped (%)', 75, { min: 1, max: 100, tooltip: 'New longs opened wholly above the PoC row, as a share of all the bar\'s new longs (shorts below it, for shorts)' });
const minVolume = input.float('Trapped share of the bar\'s volume (%)', 5, { min: 0, max: 100, tooltip: 'So a quiet handful of new positions is not a trap' });
const watch = input.int('Watch the entry for (bars)', 48, { min: 1, max: 500 });

const chart = bars();
const tape = trades();
const profile = volumeProfile();
// New positions by row this bar: `<row>L` for longs, `<row>S` for shorts.
const opened = tallyBy(8192);

// The entries: the bar each formed on, its price, its USD (above 0 for longs), and the bar price came back to it (-1 while it stands).
const KEPT = 100;
const start = state.list<number>(KEPT);
const entry = state.list<number>(KEPT);
const size = state.list<number>(KEPT);
const end = state.list<number>(KEPT);

const marks = plot.cells('Trapped rows', { candles: 'overlay', scale: 'unit' });
const longsAlert = alert('Trapped longs', { message: 'New longs trapped above the PoC' });
const shortsAlert = alert('Trapped shorts', { message: 'New shorts trapped below the PoC' });

// The theme's orange and teal, which each theme sets to read on its own background.
const LONGS = color.study(8);
const SHORTS = color.study(11);

tape.onTrade((t) => {
  const row = Math.floor(t.price / chart.rowSize + 1e-9);
  // A flip opens the other way, so it counts as an open.
  if (t.takerWhy === 'open_long' || t.takerWhy === 'flip_sl') opened.add(`${row}L`, t.size);
  else if (t.takerWhy === 'open_short' || t.takerWhy === 'flip_ls') opened.add(`${row}S`, t.size);
});

// Longs get out flat once price trades back up to their entry; shorts once it trades down to theirs.
const reached = (i: number) => (size.get(i) > 0 ? chart.high >= entry.get(i) : chart.low <= entry.get(i));

chart.onBar(() => {
  if (chart.confirmed) {
    for (let i = 0; i < start.length; i += 1) {
      if (end.get(i) < 0 && (reached(i) || chart.index - start.get(i) >= watch)) end.set(i, chart.index);
    }
    trap();
  }
  if (chart.isLast) drawEntries();
});

/** This bar's new positions on the wrong side of its PoC, when they are enough. */
function trap() {
  if (!profile.available) return;
  const bar = profile.bar();
  const poc = bar.poc({ rowSize: chart.rowSize });
  if (poc === null || !(bar.total > 0)) return;
  const long = chart.close < poc.low;
  if (!long && chart.close < poc.high) return;
  if ((long && side === 'shorts') || (!long && side === 'longs')) return;
  const pocRow = Math.round(poc.low / chart.rowSize);
  const kind = long ? 'L' : 'S';
  let all = 0;
  let total = 0;
  let cost = 0;
  let deepest = 0;
  let lowest = Infinity;
  let highest = -Infinity;
  for (const [key, amount] of opened.entries()) {
    if (key.charAt(key.length - 1) !== kind) continue;
    all += amount;
    const row = Number(key.slice(0, -1));
    // Wholly on the wrong side of the PoC row.
    if (long ? row <= pocRow : row >= pocRow) continue;
    total += amount;
    cost += amount * (row + 0.5) * chart.rowSize;
    deepest = Math.max(deepest, amount);
    lowest = Math.min(lowest, row);
    highest = Math.max(highest, row);
  }
  if (total === 0 || total / all * 100 < minShare || total / bar.total * 100 < minVolume) return;

  const tone = long ? LONGS : SHORTS;
  for (const [key, amount] of opened.entries()) {
    const row = Number(key.slice(0, -1));
    if (key.charAt(key.length - 1) !== kind || (long ? row <= pocRow : row >= pocRow)) continue;
    marks.add({ low: row * chart.rowSize, high: (row + 1) * chart.rowSize, color: color.withAlpha(tone, 0.1 + 0.3 * amount / deepest), border: null });
  }
  // One outline around where they opened, so the span reads on any footprint shading.
  marks.add({ low: lowest * chart.rowSize, high: (highest + 1) * chart.rowSize, weight: 0, color: null, border: tone });
  const price = cost / total;
  if (start.length === KEPT) for (const list of [start, entry, size, end]) list.shift();
  start.push(chart.index);
  entry.push(price);
  size.push(long ? cost : -cost);
  end.push(-1);
  if (long) longsAlert.fire(true, `New longs trapped above the PoC: ${fmt.usd(cost)} at ${fmt.price(price)}`);
  else shortsAlert.fire(true, `New shorts trapped below the PoC: ${fmt.usd(cost)} at ${fmt.price(price)}`);
}

/** Every entry as a line from its bar; the ones still standing carry a tag with how much is trapped. */
function drawEntries() {
  for (let i = 0; i < start.length; i += 1) {
    const standing = end.get(i) < 0;
    const tone = size.get(i) > 0 ? LONGS : SHORTS;
    const to = standing ? chart.index : end.get(i);
    draw.line({ from: { bar: start.get(i), price: entry.get(i) }, to: { bar: to, price: entry.get(i) }, color: color.withAlpha(tone, standing ? 1 : 0.5), width: standing ? 2 : 1, style: standing ? 'solid' : 'dashed' });
    if (standing) draw.label({ at: { bar: chart.index, price: entry.get(i) }, text: `${size.get(i) > 0 ? 'Longs' : 'Shorts'} ${fmt.usd(Math.abs(size.get(i)))}`, style: 'left', color: color.panel, textColor: tone, size: 'small' });
  }
}
```

## Next step

See the next example, [Stopped out at the extreme](https://docs.0xarchive.io/terminal/examples/stopped-at-extreme.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.