vike
Strategies

Strategies

The one Strategy trait, the Broker handle it trades through, and why a strategy written against the common denominator runs unchanged on the simulator and on the live core.

Vike Trader has ONE strategy trait. vike_model::strategy's Strategy is generic over its broker, and Broker is, in that module's own words, "the strategy's window onto whichever execution stack is running it": in a backtest the simulator core implements Broker with direct mutation (the fast path), while on the live and paper stacks the per-on_bar live context implements it by BUFFERING submissions, drained through the one live path — mint, then RiskGate, then the venue client — after the handler returns. A strategy never learns which of the two it is talking to.

The write-once rule

A strategy written as impl<B: Broker> Strategy<B> runs UNCHANGED on both stacks, and the r7 gate proves the results are bit-identical. A strategy that needs backtest-only power — schedule registration, weighted or bracket submits, sizers, multi-timeframe reads — is written against the concrete simulator broker instead, impl Strategy<SimBroker>, and the type system then documents that it is not live-portable. Portability is pinned by COMPILATION rather than by assertion: crates/vike-backtest/src/harness/registry.rs's buy_hold_mounts_on_any_broker is a generic probe function whose body is type-checked at its definition, so narrowing an impl back to one concrete broker stops compiling.

Dispatch is generic — monomorphized, never dyn — in the backtest hot loop, because a vtable per verb would regress the roughly 70 ns/bar engine. The live side may box the strategy (Box<dyn Strategy<LiveBroker> + Send>): one virtual call per closed bar is noise there, and a boxed strategy is itself a Strategy, every hook forwarding through one deref.

The Broker surface

The verb surface is deliberately the COMMON DENOMINATOR of the two stacks. Order intake is submit_market and submit_limit. Reads are position, price, equity, bars (closed bars up to and including the current step, so no look-ahead), index and now (engine time in epoch milliseconds). Engine-specific verbs stay inherent methods on the concrete broker types.

Two pre-trade liquidity reads are defaulted, so an engine with no book answers "I don't know". quote_vwap is the volume-weighted price of consuming the resting book best-level-outward; its None means the answer is unavailable or the displayed depth cannot honour the size, and the trait is explicit that it "is never a price" — a strategy must treat it as do not trade this size, not as assume the last price. depth_within_price is the sizing half: total displayed size at a limit price or better, defaulting to 0.0, which correctly sizes a book-less engine to no trade.

HftBroker and MultiHftBroker

HftBroker: Broker adds the tagged-resting-order verbs a market maker needs: submit_limit_tagged, modify_tagged (re-price in place, preserving venue queue priority) and cancel_tagged, plus a symbol-less position for the single mounted series. A tag is a strategy-chosen stable id naming one resting order, so a later modify or cancel resolves it without the strategy ever seeing a client-order-id — the registry belongs to the runtime. MultiHftBroker: HftBroker adds exactly one more read, position_of(symbol), so a correlation-aware maker can assemble the whole inventory vector.

The handlers

Every handler defaults to a no-op; implement only what the strategy needs. on_bar, on_quote_tick and on_trade_tick are skipped until index >= warmup() — the R2 gate, enforced by the engines rather than by the trait, and on_order_book sits behind it on the live side only: the backtest engine dispatches a book event before the gate, and a book event does not advance the index. Beyond on_start / on_bar / on_quote_tick / on_trade_tick / on_order_book / on_schedule / on_fill / on_stop, the seam carries the occasional-lane hooks: on_feed_status (the feed disconnected, went stale, or recovered — the "pull my quotes" trigger), on_mark (a reference mark for a series the strategy watches but is not mounted on), on_reference_quote (an L1 touch from a declared leg on a DIFFERENT venue, deliberately a distinct hook so a maker cannot mistake the reference venue's touch for its own), on_flow (per-side flow toxicity), on_order_event (the non-fill order transitions — see order lifecycle) and on_params_updated. None of those occasional-lane hooks is warmup-gated, and the trait says so hook by hook: a feed death, a reference mark, a foreign-venue touch, a toxicity reading, an order outcome and a re-tune are market, account or control facts regardless of bar count, so each fires from the first one that arrives — as on_fill always has. save_state / load_state let a strategy survive a process restart.

The fill itself is never delivered through on_order_event: quantity, price and the resulting position and equity stay on the richer on_fill path, which fires first.

Where the line runs

The recurring proposal — add the missing accessors to Broker and every strategy becomes portable — is answered in the module doc rather than re-derived: what keeps a backtest-only strategy backtest-only is not a missing READ but a CONCEPT the live stack does not have. Five of them, each owned by the simulator alone: the position sizer, the shared-cash gate's weight argument, the protective stop attached to the entry, the symbol universe, and the bar-count schedule.

Parameters

A strategy family's tuning parameters enter the bottom domain crate on exactly one criterion: the family supports a LIVE re-tune, so its params ride StrategyParams, a serde field of the runtime's journaled command lane. Two families qualify — the Avellaneda–Stoikov maker (market making) and the position controller. Every other family keeps its knobs as plain fields beside its own implementation and reads them from the harness TOML table.

Next: the registry resolves a name to a strategy, authoring in Rust writes a new one, and the templates document the ten portable names.

On this page