vike
Order flow

Footprint

The per-bar, per-price-bucket buy/sell volume grid — how trades are bucketed on the tick grid, why the bar boundary belongs to the caller, and how the app keeps a footprint for every chart bar without losing ticks.

crates/vike-orderflow/src/footprint.rs builds the footprint: a "per-bar, per-price-bucket buy/sell volume grid". Its header is explicit that it owns very little of its own arithmetic — the cells "reuse profile::{PriceBin, bucket, grid_to_cells} — the bucketing and grid→bins transform are owned by profile, not re-derived here". A footprint bar is, structurally, one bar's worth of volume profile before the POC and value area are reduced from it.

The shape

pub struct FootprintBar {
    pub bar_index: u64,
    pub cells: Vec<PriceBin>,   // PriceBin { price, buy_vol, sell_vol }, ascending by price
}

FootprintBuilder::new(tick_size) asserts a positive tick size and holds one open grid, a BTreeMap from bucket index to a (buy, sell) pair. push skips a zero-size trade, takes the classification module's signed split, and adds it to the bucket profile::bucket(price, tick_size) — "the one rounding site for all price bucketing in this crate", (price / tick_size).round(). close_bar converts the grid to ascending cells through profile::grid_to_cells, clears it, and stamps the bar with a counter that starts at 0 and advances by one per close.

Two consequences of that design are worth holding:

  • A cell's price is a grid price, not a traded price. grid_to_cells sets it to bucket × tick_size. Every trade that rounds into the same bucket lands in the same cell.
  • The bar boundary is the caller's. The builder has no clock and no threshold; push takes a trade and close_bar takes nothing. Whoever calls close_bar decides what a bar is — a time interval, a flow bar, or anything else.

from_bars is the batch twin: it takes trades already grouped per bar (&[Vec<TradeTick>]) and returns one FootprintBar per group, indexed by position. The module's tests drive the same grouped trades through push/close_bar and assert the two results equal, and pin the invariant that the sum of a bar's cells equals its buy and sell volume with zero-size trades excluded.

From footprint to profile

VolumeProfile::from_footprints(footprints, tick_size, lo, hi) in crates/vike-orderflow/src/profile.rs merges "the cells of footprints[lo..=hi] (clamped to range) into one volume-at-price profile with POC + 70% value area". Its doc comment carries one requirement: "tick_size must match the footprints' bucketing". The tests confirm that merging every bar's footprint reproduces the profile a VolumeProfileBuilder computes over the same trades directly — bins, POC and value area alike.

How the app keeps one per chart bar

crates/vike-app-core/src/orderflow.rs's OrderflowAgg is the "app-side per-bar orderflow aggregation": it buckets the drained trade tape into the chart's bars by open time and retains the per-bar footprint cells, "the substrate SP2 renders from". Its header is mostly about what happens to a tick that arrives before the chart has a bar to put it in, and the rule is that "pre-bar-0 ticks are never silently lost":

  • No bar grid at all — the tick is "unplaceable for now", and the cause is transient by construction (a Data Manager delete removes the chart and its aggregator together, so a re-add presents an aggregator whose chart has not synced yet). Those ticks are held in pending and folded in, each at its own bar by timestamp, on the first later ingest that carries a grid. The hold is bounded by PENDING_MAX_TICKS; at the bound the oldest held ticks are evicted, counted in evicted_ticks, and warned about.
  • A grid exists but the tick predates bar 0 — "unplaceable by construction", because the cell list is index-aligned to a bar list that only grows forward. Discarding is correct; discarding silently was the defect. The count is dropped_before_grid, warned on a doubling schedule.

A held tick that is still too old when the grid appears is discarded, not aged out

The header's own reasoning: staleness "resolves itself into a counted discard at flush time", so the hold needs no separate age bound.

Drawing a cell

crates/vike-chart/src/orderflow.rs's cell_text_legible decides whether a cell shows its numbers: "footprint cell numbers are drawn only when the cell is tall enough to be legible", with the threshold at 11 pixels of cell height. Below it nothing replaces them. draw_footprint (crates/vike-chart/src/render.rs) is explicit: "the tinted rect alone carries the imbalance read (no delta-bar fallback: the tint already IS the delta signal)" — the helper's own comment still mentions a delta-colored bar from the original two-tier sketch, and the renderer does not draw one. The in the GUI page covers the pane itself.

On this page