Rust SDK Overview
The tradectl SDK (tradectl-sdk) provides the building blocks for writing trading strategies — events, the Strategy trait, the Action enum, profit math, and the FFI plumbing that lets the CLI and lab load strategies as dynamic libraries.
Strategies are compiled as cdylibs. The CLI loads them via libloading. The current ABI version is STRATEGY_ABI_VERSION = 5 — strategies built against an older SDK are rejected at load.
Installation
tradectl init sets this up automatically. To add it manually:
[dependencies]
tradectl-sdk = "0.1"
[lib]
crate-type = ["cdylib"]Core Types
Events
| Type | Purpose |
|---|---|
TickerEvent | Best bid/ask snapshot (#[repr(C)] for zero-copy) |
TradeEvent | Aggregated trade (price, qty, timestamp, taker side) |
KlineEvent | OHLCV candle with interval |
DepthEvent | L2 order book update (when subscribed) |
MarketEvent | Sum type wrapping any of the above |
FillEvent | Fill notification — entry vs exit, partial flag, exit ID |
Strategy primitives
| Type | Purpose |
|---|---|
Strategy trait | on_ticker, on_trade, on_fill, name, describe, params_schema, monitor_snapshot, session_state, session_reset |
Action enum | Hold, PlaceEntry, EditEntry, CancelEntry, SetExits, AddExit, UpdateExit, RemoveExit, CloseAll |
FillResponse | List of follow-up Actions + notify flag |
ExitOrder | id, price, size, kind (Limit/Stop), delay_ms |
StrategyContext | positions, balance, PnL, trade count, depth, volume profile, can_enter gate |
PositionInfo | Accumulated position: avg_entry, quantity, total_entered, entry_count |
Side | Long, Short |
MarketType | Spot, Linear, Inverse |
MonitorSnapshot | Optional metrics surface for the live monitor UI |
See Strategy Trait for full struct definitions and the new exit-management model.
Parameters
| Type | Purpose |
|---|---|
Params | HashMap<String, serde_json::Value> — strategy params from JSON config |
ParamDef | Schema entry: key, description, default, min, max, step |
Params is JSON-typed, so it accepts numbers, strings, arrays, and nested objects — Params::get(key, default) parses to the requested type.
Loading & registration
| Macro | Use |
|---|---|
declare_strategy! | Exports tradectl_strategy() C-ABI symbol — required for every plugin |
declare_batch_strategy! | Same as above, plus a BatchFactory for sweep/shadow batch mode |
Indicators
Built into tradectl-indicators (re-exported from the SDK as tradectl_sdk::indicators). All implement the Indicator trait with O(1) streaming updates: .update(value), .value(), .ready().
| Indicator | Notes |
|---|---|
Sma | Simple Moving Average |
Ema | Exponential Moving Average (k = 2 / (period + 1)) |
Rsi | Relative Strength Index, Wilder's smoothing |
Macd | EMA(12)/EMA(26) with EMA(9) signal — .value(), .signal(), .histogram() |
BollingerBands | .value() (mid), .upper(), .lower() |
Atr | Average True Range — .update_hlc(high, low, close) for accuracy |
StdDev | Rolling standard deviation |
Vwap | Volume-weighted average — .update_pv(price, vol), .reset() at session boundaries |
See Indicators for examples.
MarketAdapter
Unified async interface for all 7 exchanges plus paper and replay adapters. Used by the live runner and the backtest engine alike.
#[async_trait]
pub trait MarketAdapter: Send + Sync {
async fn place_order(&self, request: &OrderRequest) -> ExchangeResult<Order>;
async fn cancel_order(&self, symbol: &str, order_id: &str) -> ExchangeResult<()>;
async fn edit_order(&self, symbol: &str, order_id: &str, side: OrderSide,
price: f64, quantity: Option<f64>) -> ExchangeResult<Order>;
async fn fetch_order(&self, symbol: &str, order_id: &str) -> ExchangeResult<Option<Order>>;
async fn fetch_open_orders(&self, symbol: &str) -> ExchangeResult<Vec<Order>>;
async fn get_balance(&self) -> ExchangeResult<f64>;
async fn set_leverage(&self, symbol: &str, leverage: f64) -> ExchangeResult<()>;
fn calculate_profit(&self, order: &Order) -> ProfitResult;
// ...30+ methods total: pair info, market data callbacks, depth, profit, IDs
}All methods take &self. Implementations use interior mutability (Mutex, RwLock) so the adapter can be shared as Arc<dyn MarketAdapter> across strategies and the runner without an outer mutex. This unlocks parallel REST calls.
Profit calculation
Standalone pure functions (not methods on the adapter):
pub fn calculate_linear_profit(side: Side, entry: f64, exit: f64, qty: f64,
maker_fee: f64, taker_fee: f64) -> ProfitResult;
pub fn calculate_inverse_profit(side: Side, entry: f64, exit: f64, qty: f64,
maker_fee: f64, taker_fee: f64) -> ProfitResult;
pub fn calculate_spot_profit(entry: f64, exit: f64, qty: f64,
maker_fee: f64, taker_fee: f64) -> ProfitResult;Score function
compute_score(pnl_pct, trade_count, max_dd_pct, min_trades) — used by sweeps and shadow ranking:
score = pnl% × min(trades / min_trades, 1.0) / (1 + max_dd%)BatchStrategy
The SoA (Structure-of-Arrays) trait that backs sweeps and shadow optimization. ~1000× faster than running individual strategy instances per variant.
pub trait BatchStrategy: Send {
fn process_ticker(&mut self, ticker: &TickerEvent);
fn check_trade(&mut self, trade: &TradeEvent);
fn force_close_all(&mut self, bid_price: f64);
fn results(&self) -> Vec<BatchResult>;
// ...
}See Optimization and Shadow Optimization.
Errors
pub enum ApiErrorKind {
// 17 variants, classified via .is_fatal(), .is_retryable(),
// .is_silent(), .is_persistent()
InsufficientMargin, MinNotional, RateLimit, NotFound,
InvalidSignature, Network, Timeout, /* ... */
}The runner uses these classifications to decide whether to retry, pause, or stop a strategy. 3+ persistent errors (margin/qty exceeded) within 60 s shut the strategy down across all symbols.
