vike
Market making

Market making

The SpreadMaker reference strategy, the HftBroker seam it is written against, the layers it composes and the rule that each one is byte-identical when off.

crates/vike-mm/src/lib.rs is "the HFT market-maker crate: the SpreadMaker reference strategy and its pure pricing/risk helpers, extracted from vike-core … into a dedicated, CI-gated crate that depends on vike-model alone. It does NOT own the runtime — vike-core's live core still MOUNTS the maker (via a Box<dyn Strategy<LiveBroker>>), because LiveBroker: HftBroker; only the maker code moved here, engine-agnostic over the trait."

The seam

crates/vike-model/src/strategy/mod.rs's HftBroker is "the HFT extension of Broker: the tagged-resting-order verbs a market-making strategy needs that are NOT on the portable Broker common denominator" — a signed position read plus submit_limit_tagged, modify_tagged and cancel_tagged. A tag is "a strategy-chosen stable id naming one resting order, so a later modify/cancel resolves it WITHOUT the strategy ever seeing a client-order-id". The maker is written impl<B: HftBroker> Strategy<B>, and because HftBroker: Broker "a plain Broker-only backtest engine still cannot mount them, so the type system keeps documenting the HFT requirement". The resting bid and ask carry the tags "bid" and "ask"; the maker re-prices them in place through modify, "which preserves venue queue priority", rather than cancel/replace. The portable strategy seam is the parent contract.

What composes

The crate header tells the maker's history as a stack of additions, each with the same contract: off by default, and byte-identical to the maker beneath it when off. In the SpreadMaker doc's words, "the breaker decides WHICH sides quote, the skew decides their SIZES, the style + filter decide their PRICES."

layerbuilderoff statepage
fixed mid ± half_spreadSpreadMaker::new(qty, half_spread)the basequote styles
inventory-skew sizeswith_skewskew = 0.0 ⇒ both multipliers exactly 1.0inventory skew
per-side fill-rate breakerwith_fill_breakerany of window/threshold/cooldown non-positiveinventory skew
quote-style registry + own-order filtrationwith_quote_style, with_own_order_filtration, with_own_order_bookMid, filter_own = falsequote styles
Avellaneda–Stoikov pricingwith_avellaneda_stoikovas_state = NoneAvellaneda–Stoikov
refresh tolerancewith_refresh_toleranceNone, or an all-zero bagrefresh tolerance
ladderwith_ladderlevels <= 1ladder quoting
liquidity rewardswith_liquidity_rewardsweight = 0.0liquidity rewards
flow toxicitywith_flow_toxicityboth knobs 0.0, or no reading ever deliveredshort-term alpha

The breaker is the adverse-selection guard the plain maker lacked: it "NETS the two sides against each other (round-trip netting): a bid fill later offset by an ask fill is a completed, inventory-neutral round-trip … and must NOT trip the guard — only NET one-directional accumulation on one side does." A tripped side is pulled until its cooldown expires "while the other side keeps quoting (and re-skewing) normally."

Every layer feeds one shared tick step, requote — quote emission walks its stages in order.

Config and state are split

SpreadMaker::cfg holds "the maker's whole CONFIG bag — every live-tunable knob, held as the SAME SpreadMakerParams shape the live-parameter plane transports", and it is "deliberately CONFIG-ONLY". The runtime state — the per-side resting snapshot, ladder rungs, breaker deadlines, the fill window, the own-order ladder, the learned book tick — lives in sibling fields that apply_params never touches, "so a live re-tune keeps resting orders, queue position, and learned state." Strategy::on_params_updated hot-swaps the bag without unmounting and bumps params_epoch, which "starts at 0; observable so a caller/test can confirm a re-tune actually landed."

One exception is written into the field doc: cfg.avellaneda_stoikov is "PINNED None — the live A-S params' single authority is as_state.params", re-attached by SpreadMaker::params on the way out and stripped on the way in, so no second copy can drift.

Across a restart, crates/vike-mm/src/strategy_impl.rs's SpreadMakerStateV1 persists "ONLY the per-side fill-rate breaker's suppression deadlines and (if A-S is enabled) the online estimator's accumulators" — not the fill window, not the resting-order cache (rediscovered by the reconcile pass), and not the A-S params, so "a restart with a DIFFERENT tuning should price with the new tuning, not a stale saved one."

The lanes

crates/vike-mm/src/strategy_impl.rs owns the Strategy impl. on_quote_tick and on_order_book both run requote; the book lane also learns the venue's real grid, adopted on the L1 lane "so one *_half_spread_ticks can't resolve to two different spreads." on_trade_tick feeds the κ tape, on_mark the A-S underlying anchor, on_flow the toxicity reading — none of the three touches the broker; the reaction lands "at the NEXT quote in requote".

From a profile

SpreadMaker::from_params is the registry reader: "a bare {} yields the recommended A-S maker (AsParams::default) and each present key overrides one knob." It "mirrors how vike_run::build_maker composes the live maker (fixed seed → with_quote_style → A-S), minus the reward mount-only sub-bag." The template registry carries both names — spread_maker and gueant_maker — and with_spread_model is how "the gueant_maker registry alias forces SpreadModel::Gueant so the same spread_maker knobs price with the GLFT closed form."

A typo in an enum key fails the profile — it does not fall back

ParamError exists "because Option could not say which of two things happened": each enum reader once returned None for an absent key and for an unrecognized one alike, so spread_model = "gueant_lehale" "silently ran Avellaneda–Stoikov while the profile named GLFT". A present-but-unrecognized variance_mode, horizon_mode, price_domain, spread_model, reservation_model, spread_source, kappa_mode or style now refuses to resolve. The one residual is named on read_pricing_chains_as: pricing maps to a bool, so pricing = "styel" still reads as "as".

The from_params table also names the crypto profile: an unbounded $-scale asset "wants price_domain = "unbounded", variance_mode = "raw_local", horizon_mode = "constant_tau", min_half_spread_ticks = 2, max_half_spread_ticks = 60 — the exact knobs vike_run::MakerMountConfig::crypto sets."

Three crate rules

Every fold in the crate is a naive left-to-right f64 fold with no mul_add, and crates/vike-mm/src/fits.rs's header carries the reason: "the expression shapes are bit-anchored by the tests below and must not be 'simplified'." Each module restates it as house style — "naïve folds, no mul_add, per the crate rule."

The other two are crates/vike-mm/src/own_book.rs's header. Own-book removals go through shift_remove, "never swap_remove, so queue order survives a cancel in the middle of a level", because "insertion order IS venue queue order". And every own-book timestamp is "ONE caller-chosen EVENT clock (never wall-clock)": the _ns suffixes "follow the HFT nanosecond convention", but "the code never converts or divides a timestamp", and the maker feeds it an event ts that is "epoch-MILLIS on the live quote/book/fill lanes".

The crate's second reference strategy, XemmMaker (crates/vike-mm/src/xemm/mod.rs), is complete and tested and is reached by no binary: crates/vike-run/src/xemm.rs's build_live_xemm_core has no caller, and crates/vike-strategy/src/registry.rs's strategy_by_name has no xemm arm — so no profile, flag or feature can resolve it. Cross-exchange market making carries that record.

On this page