Anvil

Strategy SDK

The Strategy trait, the order-intent boundary, and how strategies are selected at compile time without touching the platform.

The SDK is the seam between Anvil's deterministic platform and proprietary strategy logic. A strategy observes state and emits order intents; it never holds an execution handle, reads the wall clock, or performs I/O. Everything it needs arrives through a context, and everything it wants to happen leaves as an intent for the risk gateway to dispose.

The Strategy trait

A strategy implements a single trait. On each event it receives an immutable view of state and a context (deterministic time, configuration, instrument metadata) and returns the intents it wants to propose. The shapes below are representative of the SDK model; the canonical signatures live in the sdk crate's generated API.

/// Implemented by every strategy. Pure with respect to (state, event):
/// the same inputs must always produce the same intents.
pub trait Strategy {
    /// Called once at startup with validated configuration.
    fn on_start(&mut self, ctx: &StrategyContext) -> Vec<OrderIntent>;
 
    /// Called for each market event the strategy is subscribed to.
    fn on_event(
        &mut self,
        state: &StrategyView,
        event: &MarketEvent,
        ctx: &StrategyContext,
    ) -> Vec<OrderIntent>;
}

Key properties:

  • No execution handle. The trait gives the strategy no way to send an order except by returning an OrderIntent.
  • No ambient time. Time and randomness are read from ctx (deterministic clock, seeded RNG) so a replay reproduces the strategy's decisions exactly.
  • Read-only state. StrategyView is an immutable projection; the strategy cannot mutate platform state.

Order intents

An OrderIntent is a request, not a command. It describes what the strategy wants (instrument, side, price, size, time-in-force) using the platform's typed domain model — monetary values are rust_decimal-backed, never raw f64.

let intent = OrderIntent::limit(
    instrument,            // typed instrument id
    Side::Buy,
    price,                 // Decimal-backed Price
    size,                  // Decimal-backed Quantity
    TimeInForce::Gtc,
);

Every intent is journaled, then crosses the multi-stage risk gateway (position, exposure, rate, sanity). A rejected intent never reaches an exchange adapter and is recorded with its rejection reason. See Runtime architecture → Risk architecture.

Compile-time selection

Strategies are selected at build time, not loaded dynamically. Each strategy is a crate gated behind a Cargo feature (the strat_* family), and a compile-time registry binds exactly one strategy into the runner. This keeps the active strategy explicit, removes dynamic-dispatch nondeterminism, and lets the compiler enforce the platform/strategy boundary.

# Build the runner with a single strategy compiled in.
cargo build -p anvil-runner --features strat_market_making

Determinism checklist for strategy authors

  • Read time and randomness only from ctx.
  • Derive all decisions from state, event, and configuration — never from global mutable state or external services.
  • Return intents; never attempt side effects.
  • Keep on_event free of allocation-heavy work on the hot path where latency matters; the platform calls it for every subscribed event.

A strategy that follows these rules replays bit-for-bit, which is what lets the same code run in backtest, paper, and live without divergence.