NativeAlgo — Native Rust Strategies
WASM is the right boundary for third-party code. For first-party HFT — where Sequence (or your team) owns both the host and the strategy — the WASM sandbox is overhead you don't need. The NativeAlgo path compiles a strategy to a .so (Linux) or .dylib (macOS), loads it at the venue edge via libloading, and dispatches every callback through a single function-pointer call in a C-ABI vtable.
Why it's faster. Dispatch cost on the bench harness in algo-loader-native: ~1.5 ns/callback, vs 500 ns – 2 µs for the WASM path. No fuel accounting, no linear-memory copy, no sandbox boundary — just (vtable.on_book)(instance, &book, &state, &features, &mut actions). See crates/sdk/algo-loader-native/benches/dispatch.rs.
NativeAlgo executes in-process with the venue edge. There is no sandbox: a panic or a wild pointer takes down the edge. Sequence reserves NativeAlgo for code we control end-to-end (compile, sign, audit). Use WASM strategies for anything that isn't first-party.
The contract: one trait, one macro
A NativeAlgo is an implementation of the same Algo trait the WASM path uses, plus a one-line macro that emits the C-ABI vtable:
use algo_sdk::traits::Algo;
use algo_sdk::{Actions, AlgoState, Fill, L2Book, OnlineFeatures, Reject};
#[derive(Default)]
pub struct MyStrategy {
next_id: u64,
working_bid: u64,
}
impl Algo for MyStrategy {
fn on_book(
&mut self,
book: &L2Book,
_state: &AlgoState,
_features: &OnlineFeatures,
actions: &mut Actions,
) {
// your logic here — write into `actions`, return.
}
fn on_fill(&mut self, _fill: &Fill, _state: &AlgoState) {}
fn on_reject(&mut self, _reject: &Reject) {}
}
// This single line emits `extern "C" fn sequence_algo_vtable_v1() -> *const SequenceAlgoVtable`
// — the symbol `algo-loader-native` resolves via dlsym at load time.
algo_sdk::export_native_algo!(MyStrategy);That's the full surface. The same trait, the same Actions buffer, the same L2Book — only the host-side dispatcher changes.
Cargo setup
A NativeAlgo lives in a cdylib crate that depends on algo-sdk:
# Cargo.toml
[package]
name = "my-strategy"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
algo-sdk = { version = "0.4", package = "sequence-algo-sdk", default-features = false, features = ["std"] }Build the artifact:
cargo build --release
# → target/release/libmy_strategy.so (Linux)
# → target/release/libmy_strategy.dylib (macOS)The output file is the artifact the loader consumes. No additional packaging.
How the loader sees it
The host-side library is algo-loader-native. It opens the file, resolves the single exported symbol, validates the ABI version, and produces a NativeAlgoSlot that wraps one strategy instance:
use algo_loader_native::NativeAlgoLoader;
let loader = NativeAlgoLoader::open("/var/lib/sequence/strategies/libmy_strategy.so")?;
let mut slot = loader.create_slot()?;
// Hot path — dispatched through the C-ABI vtable:
slot.on_book(&book, &state, &features, &mut actions);
slot.on_fill(&fill, &state);The NativeAlgoSlot owns:
- The opaque
*mut AlgoInstanceyourcreatereturned (the macro generates this fromMyStrategy::default()). - A pointer to the
'staticvtable inside the shared library. - An
Arc<Library>that keeps the artifact mapped in process memory.
Drop order matters. When the slot is dropped, the macro-generated destroy runs before the Library Arc releases. Otherwise dropping the library first would unmap the destroy function pointer and segfault on the way out. The slot's Drop impl enforces this discipline.
Capabilities — declaring optional callbacks
The required callbacks are on_book, on_fill, on_reject. Everything else (on_nbbo, on_message, on_heartbeat, on_start, on_stop, on_pause, on_resume, on_subscribed, on_unsubscribed) is optional. The macro takes a capability list so the host knows which to actually wire up:
algo_sdk::export_native_algo!(MyStrategy, capabilities = [OnNbbo, OnHeartbeat, OnStart]);The loader reads SequenceAlgoVtable::capabilities at open time and only prepares inputs for the callbacks you've declared. Skipping OnNbbo means the host never builds the VenueBooks snapshot for your slot.
ABI version + drift detection
The vtable carries an abi_version: u32 field. The loader compares it against algo_sdk::ffi::ABI_VERSION and refuses to open the artifact if they differ:
NativeAlgoError::AbiMismatch { got: 4, expected: 5 }Treat any change to the structs in algo-contract (L2Book, Action, Fill, etc.) as an ABI break and bump the version. The compile-time assert!(size_of::<L2Book>() == 656) in algo-contract::book catches drift early.
Host callbacks (seq::*)
A strategy can call into the host during a callback — to subscribe to a new symbol, read or write a persistent parameter, look up its current position. These go through the SDK's seq module, which reads a thread-local installed by the macro at the top of every callback:
impl Algo for MyStrategy {
fn on_book(&mut self, book: &L2Book, _state: &AlgoState, _f: &OnlineFeatures, _a: &mut Actions) {
if some_condition {
// Add a symbol to my subscription set:
let _ = algo_sdk::seq::subscribe("ETH-USDT");
}
let qty = algo_sdk::seq::get_position("BTC-USDT").unwrap_or(0);
// …
}
}In no_std builds (rare for NativeAlgo) the seq::* helpers compile to no-ops that return a failure status. In normal builds they work transparently.
Dispatch latency, measured
The bench harness crates/sdk/algo-loader-native/benches/dispatch.rs measures empty on_book callback overhead:
native vtable dispatch p50 ≈ 1.5 ns (function-pointer call + 4 ref loads)
WASM dispatch (algo-rt) p50 ≈ 500 ns – 2 µs (linear memory marshalling + fuel + sandbox)That ~1000× headroom is what lets a NativeAlgo run on the venue edge's hot path without budget review.
When to reach for NativeAlgo
| Situation | Choice |
|---|---|
| Third-party code; sandbox required; deployment via your build pipeline | WASM Algo |
| First-party HFT; latency below 10 µs end-to-end matters; you sign every binary that lands | NativeAlgo |
| Research strategy you want to iterate on quickly in Python | Hosted Python (when available) |
Same trait, three deployment targets. Promote between them without changing strategy code.
See also
- Algorithm SDK overview
- L2 Order Book — the input every NativeAlgo sees
- Actions & Orders — the output buffer the loader drains after every callback
- Paper backtest harness — driving a NativeAlgo against a synthetic feed before deployment