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.
crates/vike-orderflow/src/profile.rs is the "volume-at-price profile with POC + 70% value
area". It also owns the price bucketing the whole crate uses — the
footprint module borrows it rather than re-deriving it — so this
page is where a price becomes a bin.
The shape
pub struct PriceBin { pub price: f64, pub buy_vol: f64, pub sell_vol: f64 }
pub struct VolumeProfile {
pub tick_size: f64,
pub bins: Vec<PriceBin>, // ascending by price
pub poc: f64, // the bin price with the largest buy + sell
pub value_area: (f64, f64), // (low bin price, high bin price)
}Bucketing
bucket(price, tick_size) is "the one rounding site for all price bucketing in this crate":
(price / tick_size).round(), an integer bucket index. grid_to_cells turns the sorted grid back
into ascending PriceBins whose price is bucket × tick_size — a grid price, not the price of
any particular trade. Buy and sell volume per bin come from the
classification module's signed split, so a bin's buy_vol
is aggressive buying at that grid price.
VolumeProfileBuilder::new(tick_size) asserts a positive tick size. push skips a zero-size trade
and adds the split to the trade's bucket; finish reduces a copy of the grid, so it can be read
repeatedly while trades keep arriving — the crate's forming convention in this module's shape.
from_trades is the batch twin, described in place as "separate path: collect then reduce", and
the module's parity test asserts finish() on a streamed builder equals it.
The reduction
build is the shared reducer, used by the builder and by VolumeProfile::from_footprints.
Given ascending bins:
- Total is Σ(buy + sell) over the bins.
- POC is the bin with the largest
buy + sell. Ties go to the lower price — the bins are ascending and the first maximum is kept. - Value area is a greedy one-bucket expansion from the POC. The target is
0.70 × total. While the accumulated volume is below the target, the reducer looks at the next bin above and the next bin below the current range and adds whichever holds more volume; a tie goes down, to the lower price. When only one direction remains, it takes that direction; when neither remains, it stops. The result is(bins[lo].price, bins[hi].price), and the POC is always inside it. - Empty input — no bins, or a
from_footprintsrange that is empty or out of range — producespoc = 0.0,value_area = (0.0, 0.0)and no bins.
The module's own worked example, from its tests: four trades at tick 1.0 — 5 bought at 100, 10
sold at 101, 3 bought at 102, 1 bought at 99. Total 19, so the target is 13.3. The POC is 101
(volume 10); adding 100 (volume 5) reaches 15 ≥ 13.3, so the value area is (100, 101).
The tie rules are part of the contract
Both ties — POC and value-area expansion — resolve toward the lower price. That is what makes the profile a deterministic function of the trades; two paths that reduce the same grid produce the same POC and the same value area bit for bit, which the streaming ↔ batch and footprint-merge tests rely on.
From footprints
VolumeProfile::from_footprints(footprints, tick_size, lo, hi) merges "the cells of
footprints[lo..=hi] (clamped to range) into one volume-at-price profile with POC + 70% value
area". It re-buckets each cell's price through the same bucket, so "tick_size must match the
footprints' bucketing". The tests pin that merging all of a chart's footprints equals the direct
builder over all of its trades, and that a sub-range excludes exactly the bars outside it.
That is the form the chart uses: crates/vike-chart/src/chart/price_render.rs's
paint_volume_profile reaches it through ChartState::visible_profile_shared, so the profile a
pane shows is the profile of the bars in view, computed from the same cells the footprint pane
draws. Where it renders is the in the GUI page.
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.
Depth imbalance
Book pressure over the L2 book — the depth-N size imbalance, its inverse-distance-weighted twin, and the tracker that keeps the last readable value through a one-sided book.