sequence-algo-sdk 0.4.0

Sequence Markets Algo SDK — write HFT trading algos in Rust, compile to WASM, deploy to Sequence
Documentation
//! Testing harness for native strategy development.
//!
//! Provides `AlgoHarness` that wraps a single-venue strategy and feeds it synthetic
//! market data. Only available on native builds (not WASM).
//!
//! # Example
//! ```rust,ignore
//! use algo_sdk::testing::*;
//!
//! let mut harness = AlgoHarness::new(MyAlgo::default());
//! harness.set_book(make_book(99_000_000_000, 1_00_000_000, 101_000_000_000, 1_00_000_000));
//! let actions = harness.tick();
//! assert_eq!(actions.len(), 2);
//! ```

#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
mod inner {
    use crate::{
        Actions, AlgoState, Algo, Fill, L2Book, Level, OnlineFeatures,
        Reject,
    };

    /// Build an L2Book with a single bid and ask level.
    /// Timestamp defaults to a realistic nanosecond value for latency testing.
    pub fn make_book(bid_px: u64, bid_sz: u64, ask_px: u64, ask_sz: u64) -> L2Book {
        let mut book = L2Book::default();
        if bid_px > 0 {
            book.bids[0] = Level { px_1e9: bid_px, sz_1e8: bid_sz };
            book.bid_ct = 1;
        }
        if ask_px > 0 {
            book.asks[0] = Level { px_1e9: ask_px, sz_1e8: ask_sz };
            book.ask_ct = 1;
        }
        book.recv_ns = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos() as u64;
        book
    }

    /// Build an L2Book with multiple bid and ask levels.
    /// `bids` and `asks` are slices of `(price_1e9, size_1e8)` from best to worst.
    pub fn make_depth(bids: &[(u64, u64)], asks: &[(u64, u64)]) -> L2Book {
        let mut book = L2Book::default();
        for (i, &(px, sz)) in bids.iter().enumerate().take(20) {
            book.bids[i] = Level { px_1e9: px, sz_1e8: sz };
        }
        book.bid_ct = bids.len().min(20) as u8;
        for (i, &(px, sz)) in asks.iter().enumerate().take(20) {
            book.asks[i] = Level { px_1e9: px, sz_1e8: sz };
        }
        book.ask_ct = asks.len().min(20) as u8;
        book.recv_ns = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos() as u64;
        book
    }

    fn now_ns() -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos() as u64
    }

    /// Build a Fill event.
    pub fn make_fill(order_id: u64, px_1e9: u64, qty_1e8: i64, side: i8) -> Fill {
        Fill {
            order_id,
            px_1e9,
            qty_1e8,
            recv_ns: now_ns(),
            side,
            _pad: [0; 7],
        }
    }

    /// Build a Reject event.
    pub fn make_reject(order_id: u64, code: u8) -> Reject {
        Reject {
            order_id,
            code,
            _pad: [0; 7],
        }
    }

    // ═════════════════════════════════════════════════════════════════════
    // Algo test harness
    // ═════════════════════════════════════════════════════════════════════

    /// Test harness for `Algo` trait (single-venue + OnlineFeatures).
    pub struct AlgoHarness<A: Algo> {
        pub algo: A,
        pub state: AlgoState,
        pub book: L2Book,
        pub features: OnlineFeatures,
        actions: Actions,
    }

    impl<A: Algo> AlgoHarness<A> {
        pub fn new(algo: A) -> Self {
            Self {
                algo,
                state: AlgoState::default(),
                book: L2Book::default(),
                features: OnlineFeatures::default(),
                actions: Actions::new(),
            }
        }

        pub fn set_book(&mut self, book: L2Book) { self.book = book; }
        pub fn set_position(&mut self, qty_1e8: i64, avg_entry_1e9: u64) {
            self.state.position_1e8 = qty_1e8;
            self.state.avg_entry_1e9 = avg_entry_1e9;
        }

        pub fn tick(&mut self) -> ActionSnapshot {
            self.actions.clear();
            self.algo.on_book(&self.book, &self.state, &self.features, &mut self.actions);
            ActionSnapshot::from_actions(&self.actions)
        }

        pub fn fill(&mut self, fill: Fill) {
            self.algo.on_fill(&fill, &self.state);
        }

        pub fn reject(&mut self, reject: Reject) {
            self.algo.on_reject(&reject);
        }

        pub fn shutdown(&mut self) -> ActionSnapshot {
            self.actions.clear();
            self.algo.on_shutdown(&self.state, &mut self.actions);
            ActionSnapshot::from_actions(&self.actions)
        }
    }

    // ═════════════════════════════════════════════════════════════════════
    // Action snapshot (owned, for assertions)
    // ═════════════════════════════════════════════════════════════════════

    use crate::Action;

    /// Owned snapshot of actions for test assertions.
    #[derive(Debug)]
    pub struct ActionSnapshot {
        actions: [Action; 16],
        count: usize,
    }

    impl ActionSnapshot {
        pub(crate) fn from_actions(a: &Actions) -> Self {
            let mut snap = Self {
                actions: [Action::default(); 16],
                count: a.len(),
            };
            for i in 0..a.len() {
                if let Some(action) = a.get(i) {
                    snap.actions[i] = *action;
                }
            }
            snap
        }

        /// Number of actions emitted.
        pub fn len(&self) -> usize { self.count }

        /// Whether no actions were emitted.
        pub fn is_empty(&self) -> bool { self.count == 0 }

        /// Get action at index.
        pub fn get(&self, idx: usize) -> Option<&Action> {
            if idx < self.count { Some(&self.actions[idx]) } else { None }
        }

        /// Iterator over actions.
        pub fn iter(&self) -> impl Iterator<Item = &Action> {
            self.actions[..self.count].iter()
        }

        /// Count of buy actions.
        pub fn buy_count(&self) -> usize {
            self.iter().filter(|a| a.side > 0 && a.is_cancel == 0).count()
        }

        /// Count of sell actions.
        pub fn sell_count(&self) -> usize {
            self.iter().filter(|a| a.side < 0 && a.is_cancel == 0).count()
        }

        /// Count of cancel actions.
        pub fn cancel_count(&self) -> usize {
            self.iter().filter(|a| a.is_cancel == 1).count()
        }

        /// Assert that exactly N actions were emitted.
        pub fn assert_count(&self, expected: usize) {
            assert_eq!(self.count, expected, "expected {} actions, got {}", expected, self.count);
        }

        /// Assert no actions were emitted.
        pub fn assert_empty(&self) {
            assert!(self.is_empty(), "expected no actions, got {}", self.count);
        }
    }
}

#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
pub use inner::*;