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
priceis a grid price, not a traded price.grid_to_cellssets it tobucket × 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;
pushtakes a trade andclose_bartakes nothing. Whoever callsclose_bardecides 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
pendingand folded in, each at its own bar by timestamp, on the first later ingest that carries a grid. The hold is bounded byPENDING_MAX_TICKS; at the bound the oldest held ticks are evicted, counted inevicted_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.
CVD
Cumulative volume delta — the running sum of buy minus sell volume over the trade stream, the accumulator that computes it, and the per-bar form the chart derives from footprints instead.
Volume profile
Volume at price on the tick grid, the point of control, and the 70% value area — with the tie rules that make the same trades produce the same profile every time.