The data model
The value types every crate shares — bars and ticks, the L2 book and its one delta-apply law, the per-symbol instrument grid, the fill primitive, the twenty-variant Event enum the core consumes — and the interning rule that decides which fields may be a pointer.
vike-model is rank 10 in the layering: the vocabulary every
other crate names and depends on. Almost all of it is pure value types; the exceptions touch disk —
crates/vike-model/src/state_path.rs, store_path.rs and tick_store_path.rs probe the filesystem
to answer where a project, its settings and its store root are, and change_journal.rs and
scratch.rs write under the answer. The crate header carries the parity rules every port in it
obeys — f64 end to end, expressions in the same order as the source they were ported from, no
fused multiply-add, exact-zero flat checks kept as == 0.0, timestamps as epoch-millisecond i64.
This page is the handful of its types a reader meets on every other page.
Bars and ticks
crates/vike-marketdata/src/bar.rs's Bar is one OHLCV candle, resolution-agnostic: a timestamped
price event with ts, open/high/low/close, volume, and four optionals — funding (a
perpetual's rate at this bar, if available), bid and ask (the opening best quote of the bar's
window, tick-derived and None for plain OHLCV bars), and symbol, the fully-qualified
SYMBOL.VENUE id the engine dispatch attaches. QuoteTick and TradeTick are the two tick
shapes, and both carry local_ts beside ts: the machine receive time, captured so feed latency
can later be replayed rather than modelled — it cannot be captured retroactively, and 0 means
the row was never stamped (a fixture, a backfill).
Ticks become bars in crates/vike-marketdata/src/consolidator.rs: a quote bar takes OHLC from the mid,
volume as the tick count, and the bucket's opening best bid and ask — what a next-open market
order fills against — while a trade bar takes OHLC from price, volume as summed size, and no
quote. Two shapes live there: the batch consolidate_* functions, hex-pinned against frozen
fixture bytes by the parity gate, and BarConsolidator, the
streaming twin for live folds where ticks arrive one at a time.
The L2 book, and its one delta-apply law
crates/vike-marketdata/src/orderbook.rs's L2Book is the in-core reducer: venue tasks decode depth
deltas, the book folds them, and a strategy reads the result through on_order_book. Prices are
quantized to integer ticks — price / tick, rounded half to even — so levels live in a
BTreeMap<i64, f64> keyed by tick: exact equality with no float-key hazard, ordered for O(log n)
best bid and ask. A delta whose quantity is zero removes the level, by venue convention. All its
arithmetic is add, subtract, multiply, divide and compare, so it is bit-parity eligible.
Whether an incoming delta folds, is dropped as stale, or means frames were lost is one
decision, L2Book::delta_decision, with two policies, so that live consumers and replay can never
diverge:
SeqPolicy | applies when | stale when | gap when |
|---|---|---|---|
Monotonic | seq > last_seq | seq <= last_seq (except the "no seq supplied" sentinel 0, which always applies) | never — a jump is normal on these streams, and gap detection is the bridge's own upstream rule |
Strict | seq == last_seq + 1 | seq == last_seq | anything else: a forward jump (dropped frames) or a regression (a venue restart) — the consumer must resync from a fresh snapshot |
Strict is the rule for streams whose sequence increments by exactly one per applied event, and
for the recorded BookUpdate chain every feed emits — which is what gives
tick replay its integrity rule.
The walk-the-book helpers are upper bounds, not execution guarantees
avg_px_for_quantity, quantity_for_price, simulate_fill, can_fill and fill_ratio are pure,
read-only estimates against displayed liquidity. Hidden flow, latency and queue dynamics only
make the real fill worse. Their intended consumers are the risk gate's opt-in impact veto and the
DOM ladder's cost-to-fill display, and degenerate inputs — a NaN, a crossed book — degrade rather
than panic.
SymbolProperties — the per-symbol placement grid
crates/vike-model/src/instrument.rs's SymbolProperties holds the venue-neutral order-placement
bounds every venue's REST parser fills from its own instrument endpoint: tick_size, step_size,
min_qty, max_qty, min_notional, and the later arrivals contract_size (how much underlying
one unit of quantity controls — a Deribit BTC option is one BTC per contract) and an optional
tiered tick_scheme. It lives in vike-model rather than in the bridge crates because it is pure
domain data consumed on the venue-neutral side of the seam; the per-venue parse functions stay
with their venues. An absent property is 0.0 — "the venue did not constrain this" — and the
risk gate's limits are built from this grid at mount. The
struct grows, and its doc says to construct it with functional-update syntax for that reason: one
added field once broke construction across fourteen crates. Its own page in the
instruments tree goes deeper.
The fill primitive
crates/vike-model/src/fill.rs's compute_fill is the one cost-basis primitive: how a fill
transitions a position — open, add, reduce, close, or flip — returning a FillOutcome with the new
size and average price, the closing quantity, the realized PnL on it and the leftover opened on the
far side of a flip. Callers keep their own cash, fee and trade-record bookkeeping and delegate only
this shared arithmetic, which is what stops the backtest engines and the live read-model drifting
from each other. Its branch math mirrors a frozen fixture bit for bit, and its own documentation
says not to reorder an operation, introduce a fused multiply-add, or "simplify" an expression.
The Event enum
crates/vike-model/src/events.rs defines the immutable order, fill, position and account events.
Every one is a plain value struct — Clone, no interior references — so events are copy-safe
across the channel boundary between a venue thread and the core. Event is the tagged union the
core loop consumes, serialized with a "type" tag, and it has twenty variants:
AccountState · Fill · Funding · OrderAccepted · OrderCancelRejected · OrderCanceled ·
OrderDenied · OrderExpired · OrderFilled · OrderLiquidated · OrderModified ·
OrderModifyRejected · OrderPartiallyFilled · OrderRejected · OrderSubmitted ·
OrderTriggered · PositionChanged · PositionClosed · PositionLiquidated · PositionOpened
Each has its own page under events, generated from the enum so the
list cannot drift. The order lifecycle page is where the order
events meet the state machine; OrderRequest, in the same crate, is the submission intent
itself — what a strategy, a manual ticket or a bracket builder constructs, with every field's Rust
Default pinned equal to its #[serde(default)].
The interning rule
venue and symbol on every event are interned strings — 8-byte Copy, pointer-equality,
serde-transparent. The interner never frees an entry, so interning is only sound for a field
whose set of distinct values over a process lifetime is bounded. That holds for a venue (about ten
values ever) and a symbol (bounded by the distinct instruments subscribed per session — even a
prediction-market token id is minted per market and reused on every tick of it, so the interned
set is hundreds to low thousands of markets, not the trade count).
trade_id is deliberately NOT interned, and cannot be empty
It is minted per fill, unbounded, so an interned trade id would leak without bound. It is a
TradeId newtype over a compact stack string instead. The newtype also closes a measured defect:
trade_id is the dedup key for the whole money lane, and an empty id did not dedup badly — it
skipped dedup entirely, so a re-applied fill double-booked commission and realized PnL on a live
daemon. TradeId implements no Default, so the unwrap_or_default() that produced those
empties is a compile error; TradeId::new refuses "" and hands the venue mapper the decision.
The rule generalises: only bounded, repeated label fields are interned; high-cardinality per-event
ids are not; and secrets are never interned, because an interned string cannot be zeroized.
Risk and arming
The gates between an order and the wire, in the order they run — the per-venue arming ceiling read before any credential, the credential gate behind it, the pre-trade RiskGate on every order, and the halt — with paper the default under all of them.
The history store
One Parquet store under the project, thirteen declared layouts, a gate that derives the live set from the code, many parts per day, and the resolver that decides where the store lives when nobody said.