Builtins: the footprint ladder

A footprint bar is a ladder of price rows, each carrying the volume that hit the bid and the volume that lifted the offer. These accessors read the same ladder the footprint draws. Rows are ordered from the lowest price up, and a row price is the BOTTOM of that row, so a price p belongs to the row r with r.price at or below p and p below r.price plus one tick.

SignatureReturns
cell.available() -> boolWhether this bar carries a ladder
cell.count() -> intHow many rows
cell.price(index: int) -> floatPrice at the bottom of the row
cell.bid_volume(index: int) -> floatVolume that hit the bid on that row
cell.ask_volume(index: int) -> floatVolume that lifted the offer on that row
cell.volume(index: int) -> floatBoth sides added
cell.delta(index: int) -> floatAsk volume minus bid volume on that row
cell.imbalance(index: int) -> floatRow delta divided by row volume, from -1 to 1
cell.index_at(price: float) -> intIndex of the row that contains this price, or -1
cell.volume_at(price: float) -> floatRow volume at a price
cell.bid_volume_at(price: float) -> floatBid volume at a price
cell.ask_volume_at(price: float) -> floatAsk volume at a price
cell.delta_at(price: float) -> floatRow delta at a price
cell.poc_index() -> intIndex of the highest volume row, or -1
cell.poc_price() -> floatPrice of the highest volume row
cell.max_volume() -> floatVolume on that row
cell.max_delta() -> floatLargest absolute row delta on the bar
cell.high_price() -> floatPrice of the top row
cell.low_price() -> floatPrice of the bottom row
cell.va_high([percent: float]) -> floatTop of the value area, default 70 percent
cell.va_low([percent: float]) -> floatBottom of the value area
cell.bid_imbalances(ratio: float) -> intRows whose bid beat the ask one row above by at least ratio
cell.ask_imbalances(ratio: float) -> intRows whose ask beat the bid one row below by at least ratio
cell.stacked_bid(ratio: float, min_count: int) -> boolA run of at least min_count consecutive bid imbalances
cell.stacked_ask(ratio: float, min_count: int) -> boolThe same for ask imbalances
chart.tick_size() -> floatThe instrument tick, or nan when the chart did not state one
  • Absent is not zero. A bar with no ladder answers cell.available() false, cell.count() zero, cell.index_at() and cell.poc_index() minus one, the two stacked tests false, and nan everywhere else. A bar that HAS a ladder answers zero for a price nothing traded at, because that is an observed zero.
  • cell.index_at() and cell.poc_index() answer -1 when there is no row, and the row accessors answer nan for -1, so cell.price(cell.index_at(p)) needs no guard.
  • The imbalance accessors compare a row with its diagonal neighbour: a bid against the ask one row above, an ask against the bid one row below.
  • They need to know what one row apart means, so they use the instrument tick. Where the chart states no tick they answer nan, or false for the two stacked tests, rather than calling rows two ticks apart neighbours. The price lookups answer nan and -1 the same way.
  • The heavier answers, the point of control, the value area and the imbalance counts, are computed once per bar however many times you call them.
  • Calling any cell accessor tells the chart your indicator wants the ladder, the same way the print level tape accessors do.
let has_ladder: bool = cell.available()
let poc: float = cell.poc_price()
let value_high: float = cell.va_high(70)
let value_low: float = cell.va_low(70)
let stacked: bool = has_ladder && cell.stacked_ask(3, 3)
plot.hline(poc, "#F59E0B", 1, "dotted", "POC")
plot.marker(stacked, high, "#22C55E", "square", "above", 8, 2)

Every layer of the market, Orderflow to optionflow, on one chart.

Institutional grade Orderflow data with backtesting and journaling built in. Join the waitlist for early access.

Get Access