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.
levelsis{ 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.sizeis 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.
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
levelsis the per-price fills of one aggressive order - exactly what traded at each price on this update.
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.
// 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
}
}