Sequence/docs

Overview

AlgoState contains your server-managed position and open orders. This is authoritative—you don't need to track fills yourself.

rust
pub struct AlgoState {
    // Position
    pub position_1e8: i64,         // Net position (positive = long)
    pub avg_entry_1e9: u64,        // Average entry price
    pub realized_pnl_1e9: i64,     // Realized PnL (lifetime)
    pub unrealized_pnl_1e9: i64,   // Unrealized PnL (mark-to-market)
 
    // Orders
    pub orders: [OpenOrder; 32],   // Your open orders
    pub order_ct: u8,              // Number of orders
 
    // Session & diagnostics (new in v0.2)
    pub session_pnl_1e9: i64,     // Session realized PnL (resets at UTC midnight)
    pub total_fill_count: u64,     // Lifetime fill count (never resets)
 
    // Symbol metadata (new in v0.2)
    pub symbol: SymbolMeta,        // Tick size, lot size, min qty, min notional
 
    // Risk limits (new in v0.2)
    pub risk: RiskSnapshot,        // Current risk limits for this algo
}
Note

The server calculates unrealized_pnl_1e9 using the mid price from the current book. session_pnl_1e9 resets at UTC midnight; total_fill_count is lifetime and never resets.

Tip

symbol and risk are populated by the runtime from venue pair specs and your risk configuration. All fields use zero-means-unknown semantics - helpers return safe defaults when data is unavailable.

Position

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    // Check your position
    if state.is_flat() {
        // No position
    } else if state.is_long() {
        // Long position
        let size = state.position_1e8;  // Positive
    } else {
        // Short position
        let size = state.position_1e8;  // Negative
    }
    
    // Position limit check
    if state.position_1e8.abs() > 10_000_000_000 {
        return; // Over limit, don't trade
    }
}

Position Methods

MethodReturnsDescription
is_flat()boolPosition is zero
is_long()boolPosition > 0
is_short()boolPosition < 0
total_pnl_1e9()i64Realized + unrealized PnL
get_pnl()PnlSnapshotSnapshot of realized, unrealized, total
realized_pnl_usd()f64Realized PnL as USD float
unrealized_pnl_usd()f64Unrealized PnL as USD float
total_pnl_usd()f64Total PnL as USD float
session_pnl_usd()f64Session realized PnL as USD float
Tip

See PnL & Timing for full PnL access patterns, PnlSnapshot, session PnL, and latency measurement.

Symbol Metadata

SymbolMeta gives your algo access to the trading pair's tick size, lot size, and minimum order constraints - populated from venue data at deploy time.

rust
pub struct SymbolMeta {
    pub tick_size_1e9: u64,       // Min price increment (e.g. 10_000_000 = $0.01)
    pub lot_size_1e8: u64,        // Min qty increment (e.g. 1_000 = 0.00001)
    pub min_qty_1e8: u64,         // Min order quantity
    pub min_notional_1e9: u64,    // Min order value (price × qty)
    pub price_precision: u8,      // Decimal places for prices
    pub qty_precision: u8,        // Decimal places for quantities
}

SymbolMeta Helpers

MethodReturnsDescription
round_px(px_1e9)u64Round price DOWN to nearest tick
round_qty(qty_1e8)i64Round quantity DOWN to nearest lot
check_notional(qty, px)boolTrue if order meets min notional
check_min_qty(qty)boolTrue if quantity meets minimum
Note

All helpers treat 0 as "unknown" - round_px returns the input unchanged, check_* returns true. This prevents false rejects when venue data is unavailable.

Example: Tick-Safe Quoting

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    let meta = &state.symbol;
    let mid = book.mid_px_1e9();
    let offset = mid / 2000; // 5 bps
 
    // Round prices to valid tick increments
    let bid_px = meta.round_px(mid - offset);
    let ask_px = meta.round_px(mid + offset);
 
    // Validate quantity before sending
    let qty = 10_000_000i64;
    if meta.check_min_qty(qty) && meta.check_notional(qty, bid_px) {
        self.next_id += 1;
        actions.buy(self.next_id, qty, bid_px);
        self.next_id += 1;
        actions.sell(self.next_id, qty, ask_px);
    }
}

Risk Limits

RiskSnapshot exposes the current risk limits applied to your algo. Use it to pre-check orders before sending, saving action slots and avoiding rejects.

rust
pub struct RiskSnapshot {
    pub max_position_1e8: i64,       // Max absolute position
    pub max_daily_loss_1e9: i64,     // Daily loss limit (0 = disabled)
    pub max_order_notional_1e9: u64, // Max single order value
    pub max_order_rate: u32,         // Max orders/sec
    pub reduce_only: u8,             // 1 = reduce-only mode active
}

Example: Pre-Check Before Ordering

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    let risk = &state.risk;
 
    // Skip if reduce-only mode is active
    if risk.reduce_only == 1 && state.is_flat() { return; }
 
    // Pre-check position limit
    let desired_qty = 10_000_000i64;
    if (state.position_1e8 + desired_qty).abs() > risk.max_position_1e8 {
        return; // Would exceed position limit
    }
 
    self.next_id += 1;
    actions.buy(self.next_id, desired_qty, book.bids[0].px_1e9);
}
Tip

Pre-checking risk limits in your algo saves action buffer slots (you only have 16 per tick) and avoids the round-trip overhead of a server-side reject.

Open Orders

Each order you send is tracked server-side:

rust
pub struct OpenOrder {
    pub order_id: u64,      // Your order ID
    pub px_1e9: u64,        // Limit price
    pub qty_1e8: i64,       // Signed quantity
    pub filled_1e8: i64,    // Amount filled
    pub side: i8,           // 1 = buy, -1 = sell
    pub status: u8,         // PENDING, ACKED, PARTIAL, DEAD
}

Order Status

StatusValueDescription
PENDING0Sent, awaiting exchange ack
ACKED1Acknowledged by exchange
PARTIAL2Partially filled
DEAD3Filled, cancelled, or rejected
rust
use algo_sdk::Status;
 
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    // Iterate open orders
    for i in 0..state.order_ct as usize {
        let order = &state.orders[i];
        
        if order.status == Status::ACKED || order.status == Status::PARTIAL {
            // This order is live on the exchange
            let remaining = order.remaining_1e8();
        }
    }
}

Order Methods

MethodReturnsDescription
is_live()boolACKED or PARTIAL
is_pending()boolPENDING status
remaining_1e8()i64qty - filled

State Methods

MethodReturnsDescription
has_orders()boolHas any orders
live_order_count()usizeCount of ACKED/PARTIAL orders
find_order(id)Option<&OpenOrder>Find by order ID
open_buy_qty_1e8()i64Total open buy quantity
open_sell_qty_1e8()i64Total open sell quantity

Example: Inventory Management

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    let pos = state.position_1e8;
    let max_pos = 1_000_000_000; // 10 units
    
    // Calculate available capacity
    let buy_capacity = max_pos - pos - state.open_buy_qty_1e8();
    let sell_capacity = max_pos + pos - state.open_sell_qty_1e8();
    
    // Only bid if we have buy capacity
    if buy_capacity > 10_000_000 {
        self.next_id += 1;
        actions.buy(self.next_id, 10_000_000, book.bids[0].px_1e9);
    }
    
    // Only offer if we have sell capacity
    if sell_capacity > 10_000_000 {
        self.next_id += 1;
        actions.sell(self.next_id, 10_000_000, book.asks[0].px_1e9);
    }
}

Example: Cancel Stale Orders

rust
fn on_book(&mut self, book: &L2Book, state: &AlgoState, _features: &OnlineFeatures, actions: &mut Actions) {
    let mid = book.mid_px_1e9();
    let threshold = mid / 1000; // 0.1% from mid
    
    for i in 0..state.order_ct as usize {
        let order = &state.orders[i];
        
        if !order.is_live() { continue; }
        
        // Cancel if price is too far from mid
        let distance = if order.px_1e9 > mid {
            order.px_1e9 - mid
        } else {
            mid - order.px_1e9
        };
        
        if distance > threshold {
            actions.cancel(order.order_id);
        }
    }
}

Events

Fill

rust
pub struct Fill {
    pub order_id: u64,
    pub px_1e9: u64,
    pub qty_1e8: i64,
    pub recv_ns: u64,    // Fill receive timestamp (nanoseconds)
    pub side: i8,        // 1 = buy, -1 = sell
}
 
fn on_fill(&mut self, fill: &Fill, state: &AlgoState) {
    // State already updated with this fill
    let latency_ms = fill.since_ms(self.order_sent_ns);
    log_warn!("FILL: {} @ {} latency={}ms pos={}",
        fill.qty_1e8, fill.px_1e9, latency_ms, state.position_1e8);
}

Fill Timing Helpers

MethodReturnsDescription
since_ns(start_ns)u64Elapsed nanoseconds since start_ns
since_us(start_ns)u64Elapsed microseconds
since_ms(start_ns)u64Elapsed milliseconds
Tip

Capture book.recv_ns when you send an order, then use fill.since_ms(sent_ns) to measure round-trip latency. See PnL & Timing for full examples.

Reject

rust
pub struct Reject {
    pub order_id: u64,
    pub code: u8,
}
 
fn on_reject(&mut self, reject: &Reject) {
    use algo_sdk::RejectCode;
    
    match reject.code {
        RejectCode::INSUFFICIENT_BALANCE => {
            log_error!("Not enough funds for order {}", reject.order_id);
        }
        RejectCode::INVALID_PARAMS => {
            log_error!("Invalid params on order {}", reject.order_id);
        }
        RejectCode::RATE_LIMIT => {
            log_warn!("Rate limited, backing off");
        }
        RejectCode::POSITION_LIMIT => {
            log_warn!("Position limit hit");
        }
        RejectCode::KILL_SWITCH => {
            log_error!("Kill switch active!");
        }
        _ => {
            log_warn!("Reject {}: {}", reject.order_id, reject.reason());
        }
    }
}

Reject Codes

CodeValueDescription
UNKNOWN0Unknown error
INSUFFICIENT_BALANCE1Not enough funds on exchange
INVALID_PARAMS2Invalid price, quantity, or symbol
RATE_LIMIT3Exchange rate limit hit
EXCHANGE_BUSY4Exchange temporarily unavailable
NETWORK5Network error
AUTH6Invalid API key/secret
RISK100Internal risk check failed
POSITION_LIMIT101Would exceed position limit
KILL_SWITCH102Kill switch is active
PRICE_DEVIATION103Price too far from reference
DAILY_LOSS_LIMIT104Daily loss limit breached
Tip

Use reject.reason() to get a human-readable string for logging.