Strategies
A strategy is a Rust struct that implements the Strategy trait. It receives market data events and decides when to open, close, or hold positions.
The Strategy Trait
pub trait Strategy: Send {
fn on_ticker(&mut self, ticker: &TickerEvent, ctx: &StrategyContext) -> Action;
fn name(&self) -> &str;
// Optional — have defaults
fn on_trade(&mut self, trade: &TradeEvent, ctx: &StrategyContext) -> Action { Action::Hold }
fn describe(&self) -> &str { "" }
fn params_schema(&self) -> Vec<ParamDef> { vec![] }
fn on_position_close(&mut self, close: &CloseInfo, ctx: &StrategyContext) { }
}Only on_ticker and name are required. Everything else has a default.
TickerEvent
Each ticker contains the current best bid/ask from the exchange:
#[repr(C)]
pub struct TickerEvent {
pub bid_price: f64,
pub bid_qty: f64,
pub ask_price: f64,
pub ask_qty: f64,
pub timestamp_ms: u64,
}The #[repr(C)] attribute enables zero-copy binary serialization for fast data loading.
StrategyContext
The context provides information about your current positions and account state:
pub struct StrategyContext<'a> {
pub timestamp_ms: u64,
pub book: Option<&'a TickerEvent>,
pub positions: &'a [PositionInfo],
pub balance: f64,
pub unrealized_pnl: f64,
pub realized_pnl: f64,
pub trade_count: usize,
}
pub struct PositionInfo {
pub id: u64,
pub side: Side,
pub entry_price: f64,
pub quantity: f64,
pub unrealized_pnl: f64,
pub entry_time: u64,
}Actions
pub enum Action {
Hold,
MarketOpen { side: Side, size: Option<f64> },
LimitOpen { side: Side, price: f64, size: Option<f64> },
ClosePosition { position_id: u64, reason: CloseReason },
CloseAll,
CancelPending,
}
pub enum Side { Long, Short }
pub enum CloseReason { TakeProfit, StopLoss, ForceClose }Hold— do nothingMarketOpen— open a position at market price (size: Noneuses configured default)LimitOpen— place a limit order at a specific priceClosePosition— close a specific position by ID with a reasonCloseAll— close all open positionsCancelPending— cancel all pending limit orders
declare_strategy! Macro
Every strategy must export itself via the declare_strategy! macro:
tradectl_sdk::declare_strategy!("my_strategy", MyStrategy::new);This generates the FFI entry point that the CLI and platform use to load your strategy as a dynamic library.
Params-Based Constructor
Strategies receive parameters through a Params map:
pub struct MyStrategy {
order_size: f64,
}
impl MyStrategy {
pub fn new(params: &Params) -> Self {
Self {
order_size: params.get("order_size", 0.1),
}
}
}Define the parameter schema for the dashboard and CLI:
fn params_schema(&self) -> Vec<ParamDef> {
vec![
ParamDef {
key: "order_size",
description: "Order size in quote currency",
default: 0.1,
min: None,
max: None,
step: None,
},
]
}Managers
Strategies use composition over inheritance. Managers are injected as struct fields:
TPSLManager
Manages take-profit and stop-loss orders:
use tradectl_sdk::*;
pub struct MyStrategy {
tpsl: TPSLManager,
}
impl MyStrategy {
pub fn new(params: &Params) -> Self {
Self {
tpsl: TPSLManager::new(0.02, 0.01), // 2% TP, 1% SL
}
}
}KlineManager
Builds candles from tick data:
pub struct MyStrategy {
klines: KlineManager,
}
impl MyStrategy {
pub fn new(params: &Params) -> Self {
Self {
klines: KlineManager::new(Duration::from_secs(60)), // 1m candles
}
}
}RangeBarManager
Builds range bars (fixed price movement per bar):
pub struct MyStrategy {
bars: RangeBarManager,
}
impl MyStrategy {
pub fn new(params: &Params) -> Self {
Self {
bars: RangeBarManager::new(10.0), // $10 range per bar
}
}
}Position Close Callback
React to position closes (logging, state updates):
fn on_position_close(&mut self, close: &CloseInfo, ctx: &StrategyContext) {
// close.side, close.entry_price, close.close_price
// close.profit_pct, close.profit_usd, close.reason
}