sequence-algo-sdk 0.4.0

Sequence Markets Algo SDK — write HFT trading algos in Rust, compile to WASM, deploy to Sequence
Documentation
//! Position state, orders, risk limits, symbol metadata.

// =============================================================================
// OPEN ORDER
// =============================================================================

/// Order status.
pub mod Status {
    pub const PENDING: u8 = 0; // Sent, awaiting ack
    pub const ACKED: u8 = 1; // Acknowledged by exchange
    pub const PARTIAL: u8 = 2; // Partially filled
    pub const DEAD: u8 = 3; // Filled/cancelled/rejected
}

/// Open order tracked by server.
#[derive(Debug, Clone, Copy, Default)]
#[repr(C)]
pub struct OpenOrder {
    pub order_id: u64,
    pub px_1e9: u64,
    pub qty_1e8: i64,    // Signed: positive=buy, negative=sell
    pub filled_1e8: i64, // Amount filled
    pub side: i8,        // 1=buy, -1=sell
    pub status: u8,
    pub _pad: [u8; 6],
}

impl OpenOrder {
    pub const EMPTY: Self = Self {
        order_id: 0,
        px_1e9: 0,
        qty_1e8: 0,
        filled_1e8: 0,
        side: 0,
        status: 0,
        _pad: [0; 6],
    };

    #[inline(always)]
    pub fn is_live(&self) -> bool {
        self.status == Status::ACKED || self.status == Status::PARTIAL
    }

    #[inline(always)]
    pub fn is_pending(&self) -> bool {
        self.status == Status::PENDING
    }

    #[inline(always)]
    pub fn remaining_1e8(&self) -> i64 {
        self.qty_1e8.abs() - self.filled_1e8.abs()
    }
}

// =============================================================================
// SYMBOL METADATA (server-injected, read-only)
// =============================================================================

/// Symbol trading specifications from the exchange.
/// Injected by runtime — prevents rejects from wrong lot size / tick size.
/// Fields set to 0 mean "unknown" — helpers return input unchanged.
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct SymbolMeta {
    /// Minimum price increment (1e9 scaled, e.g. 10_000_000 = $0.01). 0 = unknown.
    pub tick_size_1e9: u64,
    /// Minimum qty increment (1e8 scaled, e.g. 1_000_000 = 0.01 units). 0 = unknown.
    pub lot_size_1e8: u64,
    /// Minimum order quantity (1e8 scaled). 0 = unknown.
    pub min_qty_1e8: u64,
    /// Minimum order notional value (1e9 scaled, e.g. 10_000_000_000 = $10). 0 = no minimum.
    pub min_notional_1e9: u64,
    /// Price decimal precision (e.g. 2 = $100.00).
    pub price_precision: u8,
    /// Quantity decimal precision (e.g. 8 = 0.00000001).
    pub qty_precision: u8,
    pub _pad: [u8; 6],
}

impl Default for SymbolMeta {
    fn default() -> Self {
        Self {
            tick_size_1e9: 0,
            lot_size_1e8: 0,
            min_qty_1e8: 0,
            min_notional_1e9: 0,
            price_precision: 0,
            qty_precision: 0,
            _pad: [0; 6],
        }
    }
}

impl SymbolMeta {
    pub const EMPTY: Self = Self {
        tick_size_1e9: 0,
        lot_size_1e8: 0,
        min_qty_1e8: 0,
        min_notional_1e9: 0,
        price_precision: 0,
        qty_precision: 0,
        _pad: [0; 6],
    };

    /// Round price DOWN to nearest tick. Returns raw if tick_size == 0 (unknown).
    #[inline(always)]
    pub fn round_px(&self, px_1e9: u64) -> u64 {
        if self.tick_size_1e9 == 0 {
            return px_1e9;
        }
        (px_1e9 / self.tick_size_1e9) * self.tick_size_1e9
    }

    /// Round qty DOWN to nearest lot. Returns raw if lot_size == 0 (unknown).
    #[inline(always)]
    pub fn round_qty(&self, qty_1e8: i64) -> i64 {
        if self.lot_size_1e8 == 0 {
            return qty_1e8;
        }
        let lot = self.lot_size_1e8 as i64;
        (qty_1e8 / lot) * lot
    }

    /// Check min notional. Returns true if unknown (0) — skip check, don't false-reject.
    #[inline(always)]
    pub fn check_notional(&self, qty_1e8: i64, px_1e9: u64) -> bool {
        if self.min_notional_1e9 == 0 {
            return true;
        }
        let notional = (qty_1e8.unsigned_abs() as u128 * px_1e9 as u128 / 100_000_000) as u64;
        notional >= self.min_notional_1e9
    }

    /// Check min quantity. Returns true if unknown (0).
    #[inline(always)]
    pub fn check_min_qty(&self, qty_1e8: i64) -> bool {
        if self.min_qty_1e8 == 0 {
            return true;
        }
        qty_1e8.unsigned_abs() >= self.min_qty_1e8
    }
}

// =============================================================================
// RISK SNAPSHOT (server-injected, read-only)
// =============================================================================

/// Current risk limits — read-only view for algos.
/// Allows pre-checking orders before placing (avoids wasting actions on rejects).
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RiskSnapshot {
    /// Max absolute net position (1e8 scaled).
    pub max_position_1e8: i64,
    /// Daily loss limit (1e9 scaled). Algo paused if session PnL < -max_daily_loss. 0 = disabled.
    pub max_daily_loss_1e9: i64,
    /// Max single order notional (1e9 scaled).
    pub max_order_notional_1e9: u64,
    /// Max orders per second.
    pub max_order_rate: u32,
    /// 1 = reduce-only mode (can only close position).
    pub reduce_only: u8,
    pub _pad: [u8; 3],
}

impl Default for RiskSnapshot {
    fn default() -> Self {
        Self {
            max_position_1e8: 0,
            max_daily_loss_1e9: 0,
            max_order_notional_1e9: 0,
            max_order_rate: 0,
            reduce_only: 0,
            _pad: [0; 3],
        }
    }
}

impl RiskSnapshot {
    pub const EMPTY: Self = Self {
        max_position_1e8: 0,
        max_daily_loss_1e9: 0,
        max_order_notional_1e9: 0,
        max_order_rate: 0,
        reduce_only: 0,
        _pad: [0; 3],
    };

    /// Check if adding `delta_1e8` to `current_position_1e8` would exceed position limit.
    /// Returns true if order is safe. Returns true if limit is 0 (unknown/unlimited).
    #[inline(always)]
    pub fn check_position(&self, current_position_1e8: i64, delta_1e8: i64) -> bool {
        if self.max_position_1e8 == 0 {
            return true;
        }
        let projected = current_position_1e8.saturating_add(delta_1e8);
        projected.abs() <= self.max_position_1e8
    }
}

// =============================================================================
// ALGO STATE - Position + Orders (server-managed)
// =============================================================================

/// Maximum open orders per algo.
pub const MAX_ORDERS: usize = 32;

/// Algo state: position, orders, session stats, symbol metadata, and risk limits.
/// All managed by server — read-only from algo's perspective.
///
/// New fields (v0.2+) are appended after `_pad` for ABI backward compatibility.
/// Old WASM binaries compiled with v0.1.x read only up to `_pad` and ignore the rest.
#[derive(Clone, Copy)]
#[repr(C)]
pub struct AlgoState {
    // --- Position (v0.1) ---
    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 (v0.1) ---
    pub orders: [OpenOrder; MAX_ORDERS],
    pub order_ct: u8,
    pub _pad: [u8; 7],
    // --- Session stats (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 (v0.2) ---
    pub symbol: SymbolMeta, // Tick size, lot size, min notional
    // --- Risk limits (v0.2) ---
    pub risk: RiskSnapshot, // Max position, daily loss limit, etc.
    // --- Mesh identity (v0.3) ---
    /// This algo instance's mesh_id. 0 = not a mesh algo.
    /// Set by the runtime on deploy — the algo reads this to know its own identity.
    pub mesh_id: u64,
    /// Number of active peer mesh_ids in `mesh_peers` (max 16).
    pub mesh_peer_count: u8,
    pub _mesh_pad: [u8; 7],
    /// Mesh_ids of all peer instances of this algo (max 16).
    /// Populated by CC on deploy and updated on topology changes.
    pub mesh_peers: [u64; 16],
    // --- Symbol identity (v0.4) ---
    /// Which symbol triggered this callback.
    pub symbol_id: u16,
    pub _sym_pad: [u8; 6],
    /// Symbol name (null-padded, e.g., b"BTC-USD\0...").
    /// For multi-symbol deployments, check this to know which pair you're seeing.
    pub symbol_name: [u8; 16],
}

impl Default for AlgoState {
    fn default() -> Self {
        Self {
            position_1e8: 0,
            avg_entry_1e9: 0,
            realized_pnl_1e9: 0,
            unrealized_pnl_1e9: 0,
            orders: [OpenOrder::EMPTY; MAX_ORDERS],
            order_ct: 0,
            _pad: [0; 7],
            session_pnl_1e9: 0,
            total_fill_count: 0,
            symbol: SymbolMeta::EMPTY,
            risk: RiskSnapshot::EMPTY,
            mesh_id: 0,
            mesh_peer_count: 0,
            _mesh_pad: [0; 7],
            mesh_peers: [0; 16],
            symbol_id: 0,
            _sym_pad: [0; 6],
            symbol_name: [0; 16],
        }
    }
}

impl AlgoState {
    /// Get the symbol name as a string (e.g., "BTC-USD").
    pub fn symbol_name(&self) -> &str {
        let end = self.symbol_name.iter().position(|&b| b == 0).unwrap_or(16);
        core::str::from_utf8(&self.symbol_name[..end]).unwrap_or("")
    }
}


/// PnL snapshot in fixed-point units (1e9 = $1.00).
#[derive(Debug, Clone, Copy, Default)]
pub struct PnlSnapshot {
    pub realized_1e9: i64,
    pub unrealized_1e9: i64,
    pub total_1e9: i64,
}

impl AlgoState {
    #[inline(always)]
    pub fn is_flat(&self) -> bool {
        self.position_1e8 == 0
    }

    #[inline(always)]
    pub fn is_long(&self) -> bool {
        self.position_1e8 > 0
    }

    #[inline(always)]
    pub fn is_short(&self) -> bool {
        self.position_1e8 < 0
    }

    #[inline(always)]
    pub fn has_orders(&self) -> bool {
        self.order_ct > 0
    }

    #[inline(always)]
    pub fn live_order_count(&self) -> usize {
        let mut ct = 0;
        for i in 0..self.order_ct as usize {
            if self.orders[i].is_live() {
                ct += 1;
            }
        }
        ct
    }

    #[inline(always)]
    pub fn find_order(&self, order_id: u64) -> Option<&OpenOrder> {
        for i in 0..self.order_ct as usize {
            if self.orders[i].order_id == order_id {
                return Some(&self.orders[i]);
            }
        }
        None
    }

    #[inline(always)]
    pub fn open_buy_qty_1e8(&self) -> i64 {
        let mut sum = 0i64;
        for i in 0..self.order_ct as usize {
            let o = &self.orders[i];
            if o.is_live() && o.side > 0 {
                sum += o.remaining_1e8();
            }
        }
        sum
    }

    #[inline(always)]
    pub fn open_sell_qty_1e8(&self) -> i64 {
        let mut sum = 0i64;
        for i in 0..self.order_ct as usize {
            let o = &self.orders[i];
            if o.is_live() && o.side < 0 {
                sum += o.remaining_1e8();
            }
        }
        sum
    }

    #[inline(always)]
    pub fn total_pnl_1e9(&self) -> i64 {
        self.realized_pnl_1e9 + self.unrealized_pnl_1e9
    }

    /// Ergonomic PnL accessor for strategies that want a single call.
    #[inline(always)]
    pub fn get_pnl(&self) -> PnlSnapshot {
        PnlSnapshot {
            realized_1e9: self.realized_pnl_1e9,
            unrealized_1e9: self.unrealized_pnl_1e9,
            total_1e9: self.total_pnl_1e9(),
        }
    }

    /// Realized PnL as USD float.
    #[inline(always)]
    pub fn realized_pnl_usd(&self) -> f64 {
        self.realized_pnl_1e9 as f64 / 1e9
    }

    /// Unrealized PnL as USD float.
    #[inline(always)]
    pub fn unrealized_pnl_usd(&self) -> f64 {
        self.unrealized_pnl_1e9 as f64 / 1e9
    }

    /// Total PnL as USD float.
    #[inline(always)]
    pub fn total_pnl_usd(&self) -> f64 {
        self.total_pnl_1e9() as f64 / 1e9
    }

    /// Session realized PnL as USD float. Resets at UTC midnight.
    #[inline(always)]
    pub fn session_pnl_usd(&self) -> f64 {
        self.session_pnl_1e9 as f64 / 1e9
    }
}