Ladder quoting
The pure per-side rung builder behind the maker's N-orders-per-side mode — rung 0 is today's quote bit-for-bit, deeper rungs step outward, and the whole side is diffed to the minimal verb set each tick.
crates/vike-mm/src/ladder.rs's desired_side is the pricing half of ladder quoting: given one
side's level-0 context — the price and size today's single quote would use — it maps a
LadderParams onto that side and returns the rungs as absolute (price, size) pairs, rung 0 first
and deepest last. The data half lives one layer down, in vike-model:
crates/vike-model/src/strategy/maker_params.rs's LadderParams and LadderLevel, plus the
size-profile expansion LadderParams::rungs. The header names why the split runs that way: the
params ride the typed live-parameter payload, so they have to be a Copy scalar bag in the bottom
layer, exactly the split quote styles already keep between
QuoteStyle (data in vike-model) and book::priced (arithmetic here). Like skew, book and
refresh, the geometry is isolated so it unit-tests without a runtime or a broker.
The geometry
Rung 0 is always (level0_px, base_qty) — a laddered maker's innermost order is bit-for-bit
the quote the single-order maker would have rested. Rung k steps k · offset_step further
out, away from the reservation price: a bid rung sits that far below the level-0 bid, an ask
rung that far above the level-0 ask. The step's absolute size is the ladder's LadderOffsetUnit
scale times offset_step, and the unit is the one grid-agnostic choice in the design:
HalfSpread(the default) measures the step in multiples of the level-0 half-spread, the gap between the reservation price and level 0's quote. It needs no tick grid at all, so a1.0step reads identically on a 0..1 prediction market and a five-figure crypto book.Tickssteps by the venue grid the maker already quotes on. It needs the book's positivetick_size; on an unknown grid the ladder collapses to level 0 only.
Sizes are a multiplier of the base size, so the inventory skew
that shapes level 0 flows through every rung. LadderSizeProfile::Linear (the default) is
arithmetic growth, size_k = base · (1 + k·(ratio − 1)) — a ratio of 2 gives multipliers
1, 2, 3, 4…; Geometric is size_k = base · ratio^k — the same ratio gives 1, 2, 4, 8…. Both
put level 0 at exactly 1.0 × the base, and a size_ratio of 1.0 makes every rung the base size
under either profile. A ratio below 1 shrinks deeper rungs; the geometric profile decays without
ever reaching zero, the linear one can.
LadderParams::levels counts rungs per side including level 0. levels <= 1 is off:
LadderParams::is_active is false, the maker rests exactly today's one order per side under the
"bid"/"ask" tags, and the pre-feature verbs are reproduced byte-identically. The module is
reached only when the ladder is active (levels >= 2); nothing in it logs or allocates on the
default path.
Two guards — never place a nonsensical rung
The header lists exactly two ways a rung is refused. With no usable price scale —
unit_scale <= 0, a Ticks unit on an unknown grid or a zero or crossed half-spread — deeper
rungs would collapse onto level 0's price, so the ladder is cut to level 0 only. With a non-positive
rung size — a decaying Linear profile past zero, or a skew-floored base — the rung is not
placeable; because both profiles are monotone in k, the walk stops at the first such rung and the
returned set is the contiguous placeable prefix 0..m. The in-file tests pin both: a five-level
linear ladder at ratio 0.5 on a base of 2.0 returns two rungs, and a four-level ladder with a
zero half-spread returns one.
How the maker drives it
crates/vike-mm/src/quote.rs's requote takes the ladder branch at its stage (5), after pricing,
the reward fold, the skew and the per-side suppression verdict have already produced this tick's
(bid_px, ask_px) and sizes. The rung geometry is taken from this tick: half is
(ask_px − bid_px)/2, tick is the book view's tick_size, and each side's base_qty is its
already-skewed level-0 size. Rungs carry tags "bid0".."bidN" / "ask0".."askN", and
SpreadMaker::requote_ladder_side diffs the desired set against what is resting:
- suppressed by the fill-rate breaker: pull every resting rung of that side and place nothing — the whole-side analogue of the single path's suppression cancel, so a rung is never stranded;
- otherwise, for each desired rung
k: submit it if no rungkis resting, else modify it in place unless the per-rung refresh tolerance says the drift is churn; then cancel any resting rung beyond the desired count — the ladder shrank, through a re-tune with fewer levels or one of the guards above.
A fill on a side sets that side's refresh_stale flag, which forces every rung through the modify
arm once so a partial fill's size top-up is never read as "no change". The tolerance is never
consulted on a pull.
A rung that dies at the venue is not freed
"What is resting" is the maker's own SideState::rungs vec, written only by the diff above — and
nothing clears a rung the venue terminated. crates/vike-mm/src/strategy_impl.rs's
on_order_event, the order-death hook that frees a single quote's slot after a fill, a cancel or a
reject, matches the tags "bid"/"ask" exactly and returns on any rung tag. Its doc says so: "⚠
The LADDER path is NOT covered. … A laddered maker therefore still wedges per-rung; the ladder
is off by default (levels >= 2), and the single-quote path — the one the live mount runs — is
closed." A rung's tag carries its index into rungs, so freeing one would renumber every deeper
rung's tag away from the order actually resting under it; closing this needs a per-rung liveness
slot rather than a dense vec, which is a reshape of the diff. Until then a filled, canceled or
rejected rung stays in the vec and every later tick re-prices a terminal order through
modify_tagged.
Two retirements handle mode switches: SpreadMaker::retire_single_order cancels the leftover
"bid"/"ask" on the way into ladder mode, and SpreadMaker::retire_ladder_orders cancels
resting rungs on the way out — a guarded no-op while the rung vector is empty, which is always
the case on the never-laddered path, so the single-order tail stays byte-identical.
It composes rather than replaces
SpreadMaker::with_ladder's doc spells the composition: the style or
Avellaneda–Stoikov layer sets rung 0's price, the skew
shapes the base size every rung multiplies, and own-order filtration subtracts the rungs — the doc
recommends SpreadMaker::with_own_order_book, which tracks N orders, over the single-snapshot
filtration. The ladder rides the live-parameter plane, so on_params_updated can turn it on or
off or re-shape it without a remount.
The behaviour suite is crates/vike-mm/src/tests/ladder_quoting.rs, whose header names what it
covers: N rungs per side diffed to the minimal verb set, the single-quote reduction at
levels <= 1, per-rung tolerance, whole-side pulls, and live toggling. Where the ladder sits in the
one shared tick step is on quote emission.
Quote styles
The four pure placement styles, the book view they price from, and the two own-order filtrations that stop a maker joining or leaning on its own quote.
Refresh tolerance
The pure "is this re-quote worth the wire?" predicate — a relative basis-point band on the resting price and size that skips a modify, gates only the re-price path, and is off by default.