Tape

Every trade in order, with no minimum size and no bucketing. Where candles answer "how much traded at each price", the tape answers "in what order" - it is the source for run, retrace, and absorption logic that depends on sequence.

for (const trade of tape)

Iterate the tape directly to walk every loaded trade, oldest first. Each entry is a trade: one aggressive order, grouped from the consecutive fills that share its timestamp and side. tape.range(fromMs, toMs) is the windowed form of the same thing.

  • levels is { price, size } per price the trade filled, in fill order. A trade moves in one direction, so the first and last level are its exact price path.
  • size is the trade total - always the sum of its level quantities.
  • Prices are real prices, timestamps are ms. Merged from loaded history and the live session, so the same code works on both.
Example: circle any burst of 20 trades inside one second
const COUNT = 20
const WINDOW_MS = 1000

let recent: number[] = []
for (const trade of tape) {
  recent = recent.filter((t) => trade.timestamp - t < WINDOW_MS)
  recent.push(trade.timestamp)
  if (recent.length >= COUNT) {
    chart.mark({ price: trade.levels[0].price, time: trade.timestamp, radius: 6 })
    recent = []   // one circle per burst
  }
}

// tape.range(fromMs, toMs) walks a window instead:
interface Trade {
  timestamp: number  // ms
  side: "buy" | "sell" | "none"
  size: number       // trade total
  levels: { price: number; size: number }[]  // in fill order
}

tape.tick()

What just traded: the trades that arrived with this book update. An indicator runs on every update, so this is the slice of the tape that is new since its last run.

  • Empty on updates that carried no trade, which is most of them - the book moves far more often than it trades.
  • tape.range() cannot express this: a trade is stamped with its own trade time, not with the update's, so there is no window to ask for.
  • Same trades as the rest of the tape, so levels is the per-price fills of one aggressive order - exactly what traded at each price on this update.
Example: mark every trade of 100 or more as it prints
for (const trade of tape.tick()) {
  if (trade.size < 100) continue

  chart.mark({
    price: trade.levels[trade.levels.length - 1].price,
    time: trade.timestamp,
    radius: 4 + Math.min(trade.size / 50, 10),
    color: trade.side === 'buy' ? 'rgba(59, 130, 246, 0.8)' : 'rgba(239, 68, 68, 0.8)',
  })
}

tape.coverage()

The time ranges the tape is complete for. History loads with the chart's viewport, so a range you have not panned across may not be loaded yet.

  • tape.range() over an uncovered range returns what is loaded, not everything that traded - treat uncovered ranges as unknown rather than quiet.
Example: detect a 4-tick run with no 2-tick pullback
// candle.increment is the instrument tick size as a real price step.
const tick = candles[0].increment
const X = 4 * tick
const Y = 2 * tick

let anchor: number | null = null
let high = 0
let runStart = 0
for (const trade of tape) {
  const price = trade.levels[trade.levels.length - 1].price
  if (anchor === null || price < anchor) {
    anchor = price; high = price; runStart = trade.timestamp; continue
  }
  if (price > high) high = price
  if (high - price > Y) {
    anchor = price; high = price; runStart = trade.timestamp; continue
  }
  if (high - anchor >= X) {
    chart.box({ time1: runStart, time2: trade.timestamp, price1: anchor, price2: high })
    anchor = price; high = price; runStart = trade.timestamp
  }
}