Code API
A code indicator runs your TypeScript against replay market data on every book update, with globals for the order book, trades, footprint, and chart drawing. Copy or download the reference below into an LLM to write one, then paste it into an indicator's code window and enable it on the chart.
# OrderFlow Replay - Code Indicators API
Reference for writing chart indicators in code. Paste this whole file into an LLM,
ask it to write or edit an indicator, then paste the result into an indicator's code
window (Chart Settings -> Indicators) and enable it on the chart.
## Execution model
- An indicator is plain TypeScript. It runs once when you enable it on a chart, then re-runs on every book update for the active symbol.
- Only `state` persists across runs. Every other value is a fresh snapshot each run.
- All chart output is cleared when the indicator stops (disabled / edited / symbol change).
- There is no in-app console; the only visible output is what you draw via `chart.*`.
- Every price is a real price (e.g. 5000.25). Never scale prices.
## Globals (TypeScript declarations)
```ts
// Every price in this API is a real price (e.g. 5000.25). Never scale prices.
interface BookLevel { price: number; size: number }
declare const book: { bid: BookLevel[]; ask: BookLevel[] }
declare const trades: { buy: BookLevel[]; sell: BookLevel[] }
declare const symbol: string
declare const time: Date
declare const state: Record<string, unknown>
interface FootprintLevel { buys: number; sells: number }
interface VolumeAtPrice { buys: number; sells: number; total: number }
// A reconstructed large trade (at or above the instrument's minimum size). price is a real price.
interface LargeTrade { price: number; size: number; side: "buy" | "sell" | "none" }
// footprint.buckets and footprint.profile() are keyed by real price.
// footprint.largeTrades is keyed by candle timestamp (ms) at footprint.interval.
// footprint.candles: per display-candle { delta, volume }, keyed by the chart's candle
// timestamps - works for both time intervals and tick intervals ("133t"), so prefer it
// over aggregating buckets yourself when you need per-candle stats aligned with the chart.
interface CandleStats { delta: number; volume: number }
// footprint.increment: instrument tick size as a REAL price step (one footprint row).
declare const footprint: { buckets: Record<string, Record<string, FootprintLevel>>; candles: Record<string, CandleStats>; interval: number; increment: number; largeTrades: Record<string, LargeTrade[]>; profile: (fromMs: number, toMs: number) => Record<string, VolumeAtPrice> }
// onPress makes a drawing clickable: pressing it shows a tooltip with your text (\n for multiple lines). Use it to reveal the numbers behind a drawing, e.g. the buys/sells that sized a bar.
interface ChartOnPress { text: string }
interface ChartMarker { price: number; color?: string; radius?: number; time?: Date | number; onPress?: ChartOnPress }
// width is in px; widthTime (ms of chart time) is an alternative that scales with zoom (e.g. a % of a region: fraction * regionDurationMs). Provide one.
interface ChartHorizontalBar { price: number; width?: number; widthTime?: number; time?: Date | number; color?: string; align?: "left" | "right"; anchor?: "time" | "rightAxis" | "leftAxis"; offsetTime?: number; height?: number; borderColor?: string; borderWidth?: number; onPress?: ChartOnPress }
interface PanelPoint { time?: Date | number; value: number }
// A cell in a per-candle stat row: one candle-column-wide box under the chart. time defaults to
// current time and is floored to the chart interval. text overrides the value display (e.g. "7.4%");
// color overrides the sign-based fill (positive blue, negative red, zero gray).
interface PanelCell { time?: Date | number; value: number; text?: string; color?: string }
// cells() draws per-candle stat rows (like delta / delta % / volume diff tables). Rows only render
// while the chart is zoomed into footprint mode and the pane collapses automatically when zoomed out.
// REPLACE semantics per call (overwrite the whole row each tick). A panel should use plot OR cells, not both.
interface PanelHandle { plot: (seriesId: string, points: PanelPoint[], options?: { color?: string; type?: "line" | "histogram" }) => void; cells: (rowId: string, cells: PanelCell[], options?: { label?: string }) => void }
interface ChartLinePoint { time?: Date | number; price: number }
interface ChartHorizontalLine { price: number; color?: string; width?: number; style?: "solid" | "dashed"; onPress?: ChartOnPress }
interface ChartBox { time1: Date | number; price1: number; time2: Date | number; price2: number; color?: string; borderColor?: string; borderWidth?: number; onPress?: ChartOnPress }
interface ChartText { price: number; text: string; time?: Date | number; color?: string; align?: "left" | "center" | "right"; offsetX?: number; size?: number; onPress?: ChartOnPress }
// chart.candles(render): register a per-candle renderer. The chart invokes it for
// each VISIBLE candle on each frame with the aggregated candle view - no history
// loops needed, no per-tick cost. draw.rectangle places rows by price and fractions of
// the candle column ([from, from+width], defaults full width); draw.text anchors
// at 'at' (the same column fraction, default 0.5 = center) + offsetX px, so text
// can be kept inside a sub-region when a renderer splits the column into a
// profile and a delta histogram. This is the ONLY footprint renderer - whatever
// your script draws here is what the chart shows, candle included.
// showRowText / showSummary are the chart's readability gates: honour them or
// the numbers turn to mush when zoomed out.
// TEXT SIZE: draw.text's `size` is a BASE size at reference zoom, not fixed px.
// `scale` picks the axis that stretches it: 'y' (default) grows the text as you
// stretch the price axis / rows get taller, 'x' as candle columns get wider,
// 'both' takes the smaller of the two, 'fixed' pins it to exactly `size` px.
// Omitting `size` takes the chart's automatic sizing, which is CAPPED at ~0.7 *
// row height - on short rows the numbers shrink and leave the column empty,
// which looks like a huge gap between candles. Pass a size with scale 'x' to
// tie text to the column width instead, so it fills the column at any zoom.
interface CandleViewLevel { buys: number; sells: number }
interface CandleView { time: number; interval: number; increment: number; widthPx: number; rows: Record<string, CandleViewLevel>; delta: number; volume: number; poc: number; maxLevelVolume: number; maxLevelDelta: number; open: number; high: number; low: number; close: number; showCandle: boolean; showRowText: boolean; showSummary: boolean; numberDisplay: "sellBuy" | "deltaVol" }
interface CandleDrawRectangle { price: number; price2?: number; from?: number; width?: number; color: string; borderColor?: string; borderWidth?: number }
type TextScale = "fixed" | "x" | "y" | "both"
interface CandleDrawText { price: number; text: string; color?: string; size?: number; scale?: TextScale; at?: number; align?: "left" | "center" | "right"; offsetX?: number }
interface CandleDraw { rectangle: (rectangle: CandleDrawRectangle) => void; text: (text: CandleDrawText) => void }
// chart.frame(render) runs once per frame on BOTH charts, in absolute coordinates
// (real prices, ms timestamps - no bucket flooring), clipped to the plot.
// On the footprint chart view.candles holds the visible candles and view.trades is
// empty; on the HEATMAP (its own gear, its own script) it is the other way round -
// there are no candles, and view.trades is every trade in the visible range,
// UNFILTERED, so filtering it is the trade-size control - view.minTradeSize is the
// instrument's own floor (ES 20, most instruments 1) if you want its default.
// Scale radii by view.msPerPx (min(1, baseline / view.msPerPx)) to keep dots readable zoomed out.
// draw.circle's onPress makes a dot clickable and reveals your text (heatmap only).
interface FrameTrade { time: number; price: number; size: number; side: "buy" | "sell" | "none" }
interface FrameView { time: number; candles: CandleView[]; minPrice: number; maxPrice: number; startTime: number; endTime: number; increment: number; rowHeight: number; msPerPx: number; trades: FrameTrade[]; minTradeSize: number }
interface FrameDrawLine { points: { time: number; price: number }[]; color?: string; width?: number; style?: "solid" | "dashed" }
interface FrameDrawHLine { price: number; color?: string; width?: number; style?: "solid" | "dashed" }
interface FrameDrawRectangle { price1: number; price2: number; time1?: number; time2?: number; color?: string; borderColor?: string; borderWidth?: number }
interface FrameDrawText { price: number; text: string; time?: number; color?: string; size?: number; scale?: TextScale; align?: "left" | "center" | "right"; offsetX?: number }
interface FrameDrawCircle { time: number; price: number; radius: number; color?: string; borderColor?: string; borderWidth?: number; onPress?: ChartOnPress }
interface FrameDraw { line: (line: FrameDrawLine) => void; hline: (line: FrameDrawHLine) => void; rectangle: (rectangle: FrameDrawRectangle) => void; text: (text: FrameDrawText) => void; circle: (circle: FrameDrawCircle) => void }
// chart.condenseLevel / numberDisplay: chart display config, honored only in the
// chart SETTINGS script (gear panel). They shape the DATA a renderer receives
// (row merging, which pair of numbers is meaningful); the LOOK is entirely your
// chart.candles renderer.
// All four config values below are read back after every run of the settings
// script, and a line you leave out resolves to its default - so deleting one
// reverts that setting rather than keeping whatever was last set.
// chart.columnWidth: px width of one candle column - the space draw.rectangle's
// `from`/`width` and draw.text's `at` fractions divide up. Set it when a layout
// needs room (e.g. a profile beside a delta histogram); 0 follows the zoom.
// Applied when the settings script runs (save, preset pick, symbol or interval
// change); zooming afterwards works normally, up to this width.
// chart.columnGap: px of empty space between neighbouring columns. It comes out
// of the drawable span, so a smaller gap means wider rows; 0 keeps the automatic
// gap, which scales with the chart's size as well as the column.
declare const chart: { mark: (marker: ChartMarker) => void; panel: (options: { id: string; height?: number }) => PanelHandle; horizontalBar: (bar: ChartHorizontalBar) => void; line: (points: ChartLinePoint[], options?: { color?: string; width?: number; style?: "solid" | "dashed"; onPress?: ChartOnPress }) => void; horizontalLine: (line: ChartHorizontalLine) => void; box: (box: ChartBox) => void; text: (text: ChartText) => void; candles: (render: (candle: CandleView, draw: CandleDraw) => void) => void; frame: (render: (view: FrameView, draw: FrameDraw) => void) => void; condenseLevel?: number; numberDisplay?: "sellBuy" | "deltaVol"; columnWidth?: number; columnGap?: number }
// use('VWAP'): import a library indicator so it runs on this chart. Honored only in
// the chart SETTINGS script (gear panel); ignored in indicators. Resolves by
// indicator id first, then exact name. Can be conditional (the import set is
// re-evaluated live), e.g. only enable an indicator above a volume threshold.
declare function use(name: string): void
```
## Bindings
### book
Resting limit orders, best-first. `book.bid` / `book.ask` are `BookLevel[]`. `book.bid[0]` / `book.ask[0]` are best bid / ask. `price` is a real price, `size` is resting quantity. Zero-size levels are removed.
### trades
Executed volume for the current update only (this-tick delta), highest price first. `trades.buy` / `trades.sell` are `BookLevel[]`. Both are empty on book-only ticks (no trade).
### footprint
Accumulated per-candle traded volume for the active symbol (running history, unlike `trades`).
- `footprint.buckets`: `{ [candleTimestampMs]: { [price]: { buys, sells } } }`, at the 5s base bucket (per display candle on tick intervals like `"133t"`).
- `footprint.candles`: `{ [candleTimestampMs]: { delta, volume } }` per DISPLAY candle, keyed by the chart's rendered candle timestamps. Works for both time intervals and tick intervals - prefer it over aggregating `buckets` yourself for per-candle stats (tick-interval candle keys are anchored, NOT multiples of the interval, so manual flooring breaks there).
- `footprint.interval`: display interval in ms (Code-panel runs fall back to the 30s base; on tick intervals this is synthetic ms per candle).
- `footprint.largeTrades`: `{ [candleTimestampMs]: { price, size, side }[] }` keyed at `footprint.interval` buckets. Reconstructed large trades (venue child fills merged back into their original order), merged from historical data and the live tape, already filtered at the instrument's minimum large-trade size. `side` is `"buy"` | `"sell"` | `"none"`. The seeded "Big Trades" indicator draws these as size-scaled `chart.mark` circles.
- `footprint.profile(fromMs, toMs)`: `{ [price]: { buys, sells, total } }` summing base buckets whose start timestamp is in `[from, to]`. Whole loaded range: `footprint.profile(0, time.getTime())`.
### time / symbol / state
`time` is the replay clock `Date`. `symbol` is the active symbol string. `state` is a `Record<string, unknown>` that persists across runs within a single Run; use it to remember data between ticks.
### chart
- `chart.mark({ price, color?, radius?, time?, onPress? })`: circle at a price/time. Deduped by `price|timestamp`, capped at 5000 per symbol.
- `chart.horizontalBar({ price, width?, widthTime?, time?, color?, align?, anchor?, height?, borderColor?, borderWidth?, onPress? })`: one horizontal bar; a border draws only if `borderColor` is set (`borderWidth` px defaults 1) - e.g. outline the POC. the building block for a volume profile. Provide `width` (px, fixed) or `widthTime` (ms of chart time, scales with zoom - use `fraction * regionDurationMs` to size a bar as a % of a box/session/N-candle region); `widthTime` wins if both set. `anchor` is `"time"` (default, candle column) | `"rightAxis"` | `"leftAxis"`. Replace-per-tick: redraw the whole set each run, do NOT accumulate bars in `state`. Capped at 5000.
- `chart.line(points, { color?, width?, style?, onPress? })`: polyline on the price chart (VWAP, moving averages). `points` is `{ time?, price }[]`, needs >= 2 points. `style` is `"solid"` (default) | `"dashed"`. Replace-per-tick: redraw the whole line each run, do NOT accumulate points in `state`.
- `chart.horizontalLine({ price, color?, width?, style?, onPress? })`: full-width horizontal line at a price (support/resistance, price levels). `style` is `"solid"` (default) | `"dashed"`. Shares the `chart.line` frame, so replace-per-tick applies: redraw each run, do NOT accumulate in `state`.
- `chart.box({ time1, price1, time2, price2, color?, borderColor?, borderWidth?, onPress? })`: filled rectangle / selection highlight spanning a time range and price range (a zone reaching a high and low across a time window). Times are `Date | number` (default current `time`), prices are real; corners are order-independent. `color` (fill) defaults translucent blue; a border draws only if `borderColor` is set. Replace-per-tick: redraw the whole set each run, do NOT accumulate in `state`.
- `chart.panel({ id, height? })` returns `{ plot(seriesId, points, { color?, type? }), cells(rowId, cells, { label? }) }`: a sub-panel below the price chart. `points` is `{ time?, value }[]`; `type` is `"line"` (default) | `"histogram"`. `plot` replaces the whole series each call, so DO accumulate points in `state`. Capped at 5000 points per series.
- `panel.cells(rowId, cells, { label? })`: per-candle stat rows (a table under the chart: one colored box per candle column, e.g. delta / delta % / volume diff). `cells` is `{ time?, value, text?, color? }[]`; `time` is floored to the chart interval so each cell aligns with a candle column. Fill is sign-based (positive blue, negative red, zero gray) unless `color` is set; `text` overrides the displayed value (e.g. `"7.4%"`); `label` draws in the right axis margin. Rows only render while the chart is zoomed into footprint mode (the pane collapses when zoomed out). Replace-per-call: rebuild the whole row each run. A panel should use `plot` OR `cells`, not both; a cells panel sizes from its row count (20px per row) and ignores `height`. Capped at 5000 cells per row.
- `onPress`: any drawing (`mark`, `horizontalBar`, `line`, `horizontalLine`, `box`) accepts `onPress: { text }`. It makes the drawing clickable; pressing it shows a tooltip with your `text` (`\n` for multiple lines). Use it to reveal the numbers behind a drawing, e.g. the buys/sells that sized a bar.
## Example: volume profile
```ts
// Sum traded volume per price and draw buy/sell split bars pinned to the right axis.
// onPress makes each bar clickable, revealing the buys/sells behind its width.
const MAX_WIDTH = 80
const profile = footprint.profile(0, time.getTime())
const prices = Object.keys(profile)
let maxTotal = 0
for (const price of prices) {
if (profile[price].total > maxTotal) maxTotal = profile[price].total
}
if (maxTotal > 0) {
for (const price of prices) {
const { buys, sells, total } = profile[price]
const onPress = { text: `Buys: ${buys}\nSells: ${sells}` }
chart.horizontalBar({ price: Number(price), width: (total / maxTotal) * MAX_WIDTH, color: "rgba(239, 68, 68, 0.7)", anchor: "rightAxis", onPress })
chart.horizontalBar({ price: Number(price), width: (buys / maxTotal) * MAX_WIDTH, color: "rgba(59, 130, 246, 0.7)", anchor: "rightAxis", onPress })
}
}
```
Reference
Jump into each part of the Code API.
Book
Resting limit orders in the order book, best-first.
const bestBid = book.bid[0]
// { price, size }Trades
Executed volume for the current tick, split by aggressor side.
const { buy, sell } = tradesFootprint
Running per-candle traded volume, the source for volume profiles.
const profile = footprint.profile(from, to)Chart
Draw marks, bars, lines, boxes, and sub-panels onto the chart.
chart.horizontalLine({ price: 5000.25 })Settings Script
Import indicators with use() and render the chart itself: condense, number display, candle/frame renderers.
use("VWAP")
chart.condenseLevel = 2Iceberg Detection
Worked example: flag refreshing hidden size resting at a level.
// full indicator walkthroughPeriodic Volume Profile
Worked example: per-block volume profiles with range boxes.
const profile = footprint.profile(from, to)