sequence-algo-sdk 0.4.0

Sequence Markets Algo SDK — write HFT trading algos in Rust, compile to WASM, deploy to Sequence
Documentation
//! Venue identification, NBBO snapshots, and per-venue order books.

use crate::L2Book;

// =============================================================================
// MULTI-VENUE ALGO (cross-venue arbitrage, cross-venue market making)
// =============================================================================

/// Maximum number of venues in a multi-venue snapshot.
pub const MAX_VENUES: usize = 20; // 9 CEX + 7 DEX + 4 headroom

// =============================================================================
// VENUE ID CONSTANTS (wire format = CC VenueId + 1, 0 = default/local)
// =============================================================================

/// Kraken (CEX)
pub const VENUE_KRAKEN: u8 = 1;
/// Coinbase (CEX)
pub const VENUE_COINBASE: u8 = 2;
/// Binance (CEX)
pub const VENUE_BINANCE: u8 = 3;
/// Bitget (CEX)
pub const VENUE_BITGET: u8 = 4;
/// Crypto.com (CEX)
pub const VENUE_CRYPTOCOM: u8 = 5;
/// Bitmart (CEX)
pub const VENUE_BITMART: u8 = 6;
/// Generic DEX (aggregated across chains)
pub const VENUE_DEX: u8 = 7;
/// OKX (CEX)
pub const VENUE_OKX: u8 = 8;
/// Bybit (CEX)
pub const VENUE_BYBIT: u8 = 9;
/// Unknown venue (wire ID 10, maps to CC VenueId::Unknown)
pub const VENUE_UNKNOWN: u8 = 10;
/// DEX — Ethereum mainnet (Uniswap V2/V3, Curve, Balancer V2)
pub const VENUE_DEX_ETH: u8 = 11;
/// DEX — Arbitrum (Uniswap V3, Camelot)
pub const VENUE_DEX_ARB: u8 = 12;
/// DEX — Base (Uniswap V3, Aerodrome)
pub const VENUE_DEX_BASE: u8 = 13;
/// DEX — Optimism (Uniswap V3, Velodrome)
pub const VENUE_DEX_OP: u8 = 14;
/// DEX — Polygon (Uniswap V2, Balancer V2, Curve)
pub const VENUE_DEX_POLY: u8 = 15;
/// DEX — Solana (Raydium, Orca, Jupiter aggregated)
pub const VENUE_DEX_SOL: u8 = 16;
/// Hyperliquid (CEX — L1 with on-chain settlement, EIP-712 signing)
pub const VENUE_HYPERLIQUID: u8 = 17;

/// Returns `true` if this venue is a DEX (on-chain liquidity).
#[inline(always)]
pub fn is_dex(venue_id: u8) -> bool {
    venue_id == VENUE_DEX
        || (venue_id >= VENUE_DEX_ETH && venue_id <= VENUE_DEX_POLY)
        || venue_id == VENUE_DEX_SOL
}

/// Returns `true` if this venue is a CEX (centralized exchange).
#[inline(always)]
pub fn is_cex(venue_id: u8) -> bool {
    (venue_id >= VENUE_KRAKEN && venue_id <= VENUE_BYBIT && venue_id != VENUE_DEX)
        || venue_id == VENUE_HYPERLIQUID
}

/// Human-readable name for a venue ID.
pub fn venue_name(venue_id: u8) -> &'static str {
    match venue_id {
        0 => "default",
        VENUE_KRAKEN => "kraken",
        VENUE_COINBASE => "coinbase",
        VENUE_BINANCE => "binance",
        VENUE_BITGET => "bitget",
        VENUE_CRYPTOCOM => "cryptocom",
        VENUE_BITMART => "bitmart",
        VENUE_DEX => "dex",
        VENUE_OKX => "okx",
        VENUE_BYBIT => "bybit",
        VENUE_UNKNOWN => "unknown",
        VENUE_DEX_ETH => "dex-eth",
        VENUE_DEX_ARB => "dex-arb",
        VENUE_DEX_BASE => "dex-base",
        VENUE_DEX_OP => "dex-op",
        VENUE_DEX_POLY => "dex-poly",
        VENUE_DEX_SOL => "dex-sol",
        VENUE_HYPERLIQUID => "hyperliquid",
        _ => "unknown",
    }
}

/// Cross-venue NBBO snapshot delivered to multi-venue algos.
///
/// Contains the global NBBO plus per-venue BBO data in struct-of-arrays
/// layout for cache efficiency. ~480 bytes, fits in a single WASM page.
///
/// ## Venue-index convention (frozen — all paths use this):
/// - `venue_ids[slot]` = VenueId for array slot `slot` (populated by runtime, sorted ascending)
/// - `venue_bid_px[slot]`, `venue_ask_px[slot]`, etc. all use the same `slot` index
/// - `nbbo_bid_venue`, `nbbo_ask_venue` = array slot index (0..venue_ct-1), NOT VenueId
///   - Use `venue_ids[nbbo_bid_venue]` to get the VenueId of the best bidder
/// - `Action.venue_id` = VenueId VALUE (not array slot). Runtime maps via `venue_ids[]`
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct NbboSnapshot {
    // --- Global NBBO ---
    pub nbbo_bid_px_1e9: u64,
    pub nbbo_ask_px_1e9: u64,
    pub nbbo_bid_sz_1e8: u64,
    pub nbbo_ask_sz_1e8: u64,
    // --- Venue identification ---
    pub nbbo_bid_venue: u8, // array slot index of best bid venue
    pub nbbo_ask_venue: u8, // array slot index of best ask venue
    pub venue_ct: u8,       // number of active venues
    /// Symbol index for multi-symbol strategies (0 = default/single-symbol, max 255).
    pub symbol_id: u8,
    pub sequence: u32, // monotonic counter — algo can detect stale snapshots
    // --- Venue ID mapping: slot → VenueId (sorted ascending) ---
    pub venue_ids: [u8; MAX_VENUES],
    // --- Per-venue BBO (struct-of-arrays for cache efficiency) ---
    pub venue_bid_px: [u64; MAX_VENUES],
    pub venue_ask_px: [u64; MAX_VENUES],
    pub venue_bid_sz: [u32; MAX_VENUES],
    pub venue_ask_sz: [u32; MAX_VENUES],
    // --- Per-venue staleness ---
    pub venue_update_ms: [u16; MAX_VENUES], // ms since last update (0=fresh, 65535=stale)
    // --- Timestamp ---
    pub recv_ns: u64,
    /// Region ID per venue slot (0=UsEast1, 1=EuWest1, ..., 255=Unknown).
    /// Appended for backward compat: old WASM binaries compiled against the
    /// smaller struct read only their expected bytes.
    pub venue_region_ids: [u8; MAX_VENUES],
}

impl Default for NbboSnapshot {
    fn default() -> Self {
        Self {
            nbbo_bid_px_1e9: 0,
            nbbo_ask_px_1e9: 0,
            nbbo_bid_sz_1e8: 0,
            nbbo_ask_sz_1e8: 0,
            nbbo_bid_venue: 0,
            nbbo_ask_venue: 0,
            venue_ct: 0,
            symbol_id: 0,
            sequence: 0,
            venue_ids: [0; MAX_VENUES],
            venue_bid_px: [0; MAX_VENUES],
            venue_ask_px: [0; MAX_VENUES],
            venue_bid_sz: [0; MAX_VENUES],
            venue_ask_sz: [0; MAX_VENUES],
            venue_update_ms: [0; MAX_VENUES],
            recv_ns: 0,
            venue_region_ids: [0; MAX_VENUES],
        }
    }
}

impl NbboSnapshot {
    /// Whether any venue's bid > another venue's ask (crossed market = arb opportunity).
    #[inline(always)]
    pub fn is_crossed(&self) -> bool {
        self.nbbo_bid_px_1e9 > 0
            && self.nbbo_ask_px_1e9 > 0
            && self.nbbo_bid_px_1e9 > self.nbbo_ask_px_1e9
            && self.nbbo_bid_venue != self.nbbo_ask_venue
    }

    /// Get bid price for a venue slot.
    #[inline(always)]
    pub fn venue_bid(&self, slot: usize) -> u64 {
        if slot < self.venue_ct as usize {
            self.venue_bid_px[slot]
        } else {
            0
        }
    }

    /// Get ask price for a venue slot.
    #[inline(always)]
    pub fn venue_ask(&self, slot: usize) -> u64 {
        if slot < self.venue_ct as usize {
            self.venue_ask_px[slot]
        } else {
            0
        }
    }

    /// Check if a venue's data is stale.
    #[inline(always)]
    pub fn is_venue_stale(&self, slot: usize, max_ms: u16) -> bool {
        if slot < self.venue_ct as usize {
            self.venue_update_ms[slot] > max_ms
        } else {
            true
        }
    }

    /// Find the array slot for a given VenueId. Linear scan of `venue_ids[0..venue_ct]`.
    #[inline]
    pub fn slot_for_venue(&self, venue_id: u8) -> Option<usize> {
        let ct = self.venue_ct as usize;
        for i in 0..ct {
            if self.venue_ids[i] == venue_id {
                return Some(i);
            }
        }
        None
    }

    /// VenueId of the best bid venue.
    #[inline(always)]
    pub fn best_bid_venue_id(&self) -> u8 {
        self.venue_ids[self.nbbo_bid_venue as usize]
    }

    /// VenueId of the best ask venue.
    #[inline(always)]
    pub fn best_ask_venue_id(&self) -> u8 {
        self.venue_ids[self.nbbo_ask_venue as usize]
    }
}

// =============================================================================
// VENUE BOOKS (per-venue full-depth order books for multi-venue algos)
// =============================================================================

/// WASM memory offset where VenueBooks is written by the runtime.
/// Multi-venue algos read per-venue depth from this fixed address.
pub const VENUE_BOOKS_WASM_OFFSET: u32 = 0x10000; // page 2 start

/// Per-venue full-depth order books delivered to multi-venue algos.
///
/// Contains a merged cross-venue L2Book plus individual per-venue L2Books.
/// Venue ordering matches `NbboSnapshot.venue_ids[]` — same slot index,
/// same VenueId mapping.
///
/// ## DEX depth
///
/// DEX books are **synthetic**: constructed by probing AMM pools (Uniswap V2/V3,
/// Curve, Balancer, etc.) at discrete USD sizes ($10 → $500K). Prices are
/// gas-adjusted. Below $10K: single best-pool routing. Above $10K: greedy split
/// across multiple pools. Accuracy is validated every 10th emission and
/// suppressed if error exceeds 25 bps vs live requote.
///
/// ## Size
///
/// ~14 KB (at MAX_VENUES=20). Written to WASM memory at offset `VENUE_BOOKS_WASM_OFFSET` (0x10000,
/// page 2). Well within the 16 MB default WASM memory allocation.
#[derive(Clone)]
#[repr(C)]
pub struct VenueBooks {
    /// Merged cross-venue depth book (all venues aggregated, same-price sizes summed).
    /// Same data previously available as the `book` parameter in v0.2 `on_nbbo`.
    pub merged: L2Book,
    /// Number of valid per-venue books (0..MAX_VENUES).
    pub book_ct: u8,
    /// Symbol index for multi-symbol strategies (0 = default/single-symbol).
    pub symbol_id: u16,
    pub _pad: [u8; 5],
    /// Wire VenueIds for each book slot (same ordering as `NbboSnapshot.venue_ids`).
    /// Use `VENUE_KRAKEN`, `VENUE_DEX_ARB`, etc. to identify venues.
    pub venue_ids: [u8; MAX_VENUES],
    /// Region ID per venue slot (same ordering as `NbboSnapshot.venue_region_ids`).
    /// Matches `NbboSnapshot.venue_region_ids` so slots align across both structs.
    pub venue_region_ids: [u8; MAX_VENUES],
    /// Per-venue L2 order books. `books[i]` corresponds to `venue_ids[i]`.
    /// Each book has up to 20 levels per side. `bid_ct`/`ask_ct` indicate valid levels.
    /// A venue with no depth data will have `bid_ct == 0 && ask_ct == 0`.
    pub books: [L2Book; MAX_VENUES],
}

impl Default for VenueBooks {
    fn default() -> Self {
        Self {
            merged: L2Book::default(),
            book_ct: 0,
            symbol_id: 0,
            _pad: [0; 5],
            venue_ids: [0; MAX_VENUES],
            venue_region_ids: [0; MAX_VENUES],
            books: [L2Book::default(); MAX_VENUES],
        }
    }
}

impl VenueBooks {
    /// Look up the L2Book for a specific venue by VenueId.
    /// Returns `None` if the venue is not present.
    #[inline]
    pub fn book_for_venue(&self, venue_id: u8) -> Option<&L2Book> {
        let ct = self.book_ct as usize;
        for i in 0..ct {
            if self.venue_ids[i] == venue_id {
                return Some(&self.books[i]);
            }
        }
        None
    }

    /// Direct access to the L2Book at a given slot index.
    /// Returns default (empty) book if slot is out of range.
    #[inline(always)]
    pub fn book_at_slot(&self, slot: usize) -> &L2Book {
        if slot < self.book_ct as usize {
            &self.books[slot]
        } else {
            // Return the first book as a default (all zeros if no books)
            &self.books[0]
        }
    }

    /// Look up the L2Book for a specific venue by VenueId and RegionId.
    /// When the same venue appears in multiple regions (e.g. after cross-region
    /// peer linking), `book_for_venue` returns the first match which is
    /// ambiguous. This method matches both fields.
    /// Returns `None` if no (venue, region) pair is present.
    #[inline]
    pub fn book_for_venue_region(&self, venue_id: u8, region_id: u8) -> Option<&L2Book> {
        let ct = self.book_ct as usize;
        for i in 0..ct {
            if self.venue_ids[i] == venue_id && self.venue_region_ids[i] == region_id {
                return Some(&self.books[i]);
            }
        }
        None
    }

    /// Whether a venue has depth data (at least one bid or ask level).
    #[inline]
    pub fn has_depth_for(&self, venue_id: u8) -> bool {
        self.book_for_venue(venue_id)
            .map_or(false, |b| b.bid_ct > 0 || b.ask_ct > 0)
    }

    /// Get the VenueId at a given slot.
    #[inline(always)]
    pub fn venue_id_at(&self, slot: usize) -> u8 {
        if slot < self.book_ct as usize {
            self.venue_ids[slot]
        } else {
            0
        }
    }

    /// Number of CEX venues with book data.
    #[inline]
    pub fn cex_count(&self) -> usize {
        let ct = self.book_ct as usize;
        (0..ct).filter(|&i| is_cex(self.venue_ids[i])).count()
    }

    /// Number of DEX venues with book data.
    #[inline]
    pub fn dex_count(&self) -> usize {
        let ct = self.book_ct as usize;
        (0..ct).filter(|&i| is_dex(self.venue_ids[i])).count()
    }
}