sequence-algo-sdk 0.4.0

Sequence Markets Algo SDK — write HFT trading algos in Rust, compile to WASM, deploy to Sequence
Documentation
//! DEX pool books, AMM state, and pool metadata.

use crate::L2Book;

// =============================================================================
// Per-Pool Books (A6 — separate from NBBO hot path)
// =============================================================================

/// Maximum number of individual pool slots tracked per symbol.
pub const MAX_POOLS: usize = 32;

/// WASM memory offset where PoolBooks is written by the runtime.
/// Located after VenueBooks to avoid overlap.
pub const POOL_BOOKS_WASM_OFFSET: u32 = 0x14000;

/// Per-pool metadata: compact identity + fee data for a single DEX pool.
///
/// Fixed-size (48 bytes). Fits 32 pools in ~1.5KB within the PoolBooks region.
/// Layout: address(32) + pair_index(2) + fee_bps(2) + venue_id(1) + protocol_id(1) +
///         gas_cost_1e5(2) + gas_units(2) + gas_price_gwei(2) + native_price_cents(4) = 48.
#[derive(Clone, Copy)]
#[repr(C)]
pub struct PoolMeta {
    /// Pool contract address: 32 bytes (EVM uses first 20, Solana uses all 32).
    pub address: [u8; 32],
    /// Pair index for multi-asset pools (0 for standard 2-token pools).
    pub pair_index: u16,
    /// Pool swap fee in basis points (AMM fee, not gas).
    pub fee_bps: u16,
    /// Venue ID (VENUE_DEX_ARB, VENUE_DEX_SOL, etc.)
    pub venue_id: u8,
    /// Protocol: 0=unknown, 1=uniswap_v2, 2=uniswap_v3, 3=curve,
    /// 4=balancer_v2, 5=aerodrome, 6=velodrome, 7=camelot,
    /// 8=raydium_clmm, 9=orca_whirlpool
    pub protocol_id: u8,
    // ── Pre-computed gas cost (convenience) ──
    /// Estimated total execution cost, 1 unit = $0.00001 (max $0.65).
    /// Pre-computed: gas_units_1k × gas_price_gwei × native_price / 1e9 × 1e5.
    /// Use `gas_cost_usd()` for f64 conversion. 0 = unknown.
    pub gas_cost_1e5: u16,
    // ── Raw gas inputs (for custom models) ──
    /// Protocol gas units in thousands (e.g., 180 = 180,000 gas). 0 = unknown.
    /// From edge's protocol.estimate_gas(). Use `gas_units()` for full value.
    pub gas_units_1k: u16,
    /// Current gas price on this chain in gwei (base + priority). 0 = unknown.
    pub gas_price_gwei: u16,
    /// Native token price in deci-dollars (1 unit = $0.10). ETH=$3500 → 35000. Max $6,553.50.
    /// For exact native price, use `ChainFeeTable.fee_for_chain().native_price_1e9`.
    pub native_price_deci_usd: u16,
    pub _pad: [u8; 2],
}

impl Default for PoolMeta {
    fn default() -> Self {
        Self {
            address: [0; 32],
            pair_index: 0,
            fee_bps: 0,
            venue_id: 0,
            protocol_id: 0,
            gas_cost_1e5: 0,
            gas_units_1k: 0,
            gas_price_gwei: 0,
            native_price_deci_usd: 0,
            _pad: [0; 2],
        }
    }
}

impl PoolMeta {
    /// Pre-computed gas execution cost in USD (f64). 0.0 if unknown.
    #[inline(always)]
    pub fn gas_cost_usd(&self) -> f64 {
        self.gas_cost_1e5 as f64 / 100_000.0
    }

    /// Raw gas units (e.g., 180,000 for Uniswap V3). 0 if unknown.
    #[inline(always)]
    pub fn gas_units(&self) -> u64 {
        self.gas_units_1k as u64 * 1000
    }

    /// Native token price in USD (e.g., 3500.0 for ETH). 0.0 if unknown.
    #[inline(always)]
    pub fn native_price_usd(&self) -> f64 {
        self.native_price_deci_usd as f64 / 10.0
    }

    /// Compute gas cost from raw inputs (for custom models).
    /// `gas_units × gas_price_gwei × native_price_usd / 1e9`
    #[inline]
    pub fn compute_gas_cost_usd(&self) -> f64 {
        self.gas_units() as f64 * self.gas_price_gwei as f64 * self.native_price_usd() / 1e9
    }

    /// Total cost to execute on this pool: swap fee + gas, in bps of a given notional.
    #[inline]
    pub fn total_cost_bps(&self, notional_1e9: u64) -> f64 {
        let fee = self.fee_bps as f64;
        let gas_bps = if notional_1e9 > 0 {
            (self.gas_cost_usd() * 10_000.0 * 1_000_000_000.0) / notional_1e9 as f64
        } else {
            0.0
        };
        fee + gas_bps
    }
}

/// Per-pool depth books delivered to multi-venue algos.
///
/// Written to WASM memory at `POOL_BOOKS_WASM_OFFSET` (0x14000) on a
/// separate cadence from NBBO — typically per-block, not per-tick.
/// Algos read this on demand during `on_nbbo()` via fixed WASM offset.
///
/// ~23 KB total. Conditional memcpy: skipped when `pool_ct == 0`.
#[derive(Clone)]
#[repr(C)]
pub struct PoolBooks {
    /// Number of valid pool slots (0..MAX_POOLS).
    pub pool_ct: u8,
    pub _pad: [u8; 7],
    /// Metadata for each pool slot.
    pub metas: [PoolMeta; MAX_POOLS],
    /// L2 books for each pool. `books[i]` corresponds to `metas[i]`.
    pub books: [L2Book; MAX_POOLS],
}

impl Default for PoolBooks {
    fn default() -> Self {
        Self {
            pool_ct: 0,
            _pad: [0; 7],
            metas: [PoolMeta::default(); MAX_POOLS],
            books: [L2Book::default(); MAX_POOLS],
        }
    }
}

impl PoolBooks {
    /// Look up the L2Book for a specific pool by address and pair_index.
    #[inline]
    pub fn book_for_pool(&self, addr: &[u8; 32], pair_index: u16) -> Option<&L2Book> {
        let ct = self.pool_ct as usize;
        for i in 0..ct {
            if self.metas[i].address == *addr && self.metas[i].pair_index == pair_index {
                return Some(&self.books[i]);
            }
        }
        None
    }

    /// Direct access to the L2Book at a given slot index.
    #[inline(always)]
    pub fn book_at_slot(&self, slot: usize) -> &L2Book {
        if slot < self.pool_ct as usize {
            &self.books[slot]
        } else {
            &self.books[0]
        }
    }

    /// Direct access to the PoolMeta at a given slot index.
    #[inline(always)]
    pub fn meta_at_slot(&self, slot: usize) -> &PoolMeta {
        if slot < self.pool_ct as usize {
            &self.metas[slot]
        } else {
            &self.metas[0]
        }
    }
}

// =============================================================================
// POOL STATE TABLE (raw AMM state for strategy-side pricing)
// =============================================================================

/// WASM offset for PoolStateTable — after ExecutionPlanBuffer.
/// ExecutionPlanBuffer at 0x22200 (2120 bytes) ends at ~0x22A48. Safe gap to 0x23000.
pub const POOL_STATE_TABLE_WASM_OFFSET: u32 = 0x23000;

/// Maximum pool states (1:1 correspondence with PoolBooks).
pub const MAX_POOL_STATES: usize = 32;

/// Pool type discriminant for PoolAmm.
pub mod pool_type {
    /// Constant-product AMM (Uniswap V2, Raydium V4/CPMM).
    pub const V2: u8 = 0;
    /// Concentrated liquidity (Uniswap V3, Raydium CLMM, Orca Whirlpool).
    pub const V3_CLMM: u8 = 1;
    /// Stableswap (Curve, multi-asset).
    pub const STABLESWAP: u8 = 2;
}

/// Raw AMM state for a single pool. Slot `i` corresponds to `PoolBooks.metas[i]`.
///
/// Strategies use this to compute swap output from the pricing function directly
/// rather than relying on pre-computed synthetic L2Books.
///
/// - **V2 pools**: use `reserve0()`, `reserve1()`, `fee_ppm`
/// - **V3/CLMM pools**: use `sqrt_price_x64`, `tick`, `liquidity`, `fee_ppm`, `tick_spacing`
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct PoolAmm {
    // ── V2 reserves (split u128 for WASM portability) ──
    /// Lower 64 bits of reserve0.
    pub reserve0_lo: u64,
    /// Upper 64 bits of reserve0 (usually 0 for practical pools).
    pub reserve0_hi: u64,
    /// Lower 64 bits of reserve1.
    pub reserve1_lo: u64,
    /// Upper 64 bits of reserve1.
    pub reserve1_hi: u64,

    // ── V3/CLMM state ──
    /// sqrt(price) in Q64.64 fixed-point (converted from Q64.96 by CC).
    /// To get price: `(sqrt_price_x64 as f64 / 2^64)^2`.
    pub sqrt_price_x64: u128,    // offset 32, 16-byte aligned ✓
    /// Active liquidity at the current tick.
    pub liquidity: u128,          // offset 48, 16-byte aligned ✓

    /// Current tick index. Price at tick `t` ≈ 1.0001^t.
    pub tick: i32,
    /// Tick spacing (V3 only: 1, 10, 60, 200).
    pub tick_spacing: i32,

    // ── Metadata ──
    /// Swap fee in parts-per-million (3000 = 0.3%).
    pub fee_ppm: u32,
    /// Pool type: 0=V2, 1=V3/CLMM, 2=stableswap.
    pub pool_type: u8,
    pub _pad0: [u8; 3],
    /// Block/slot number when this state was observed.
    pub last_block: u64,
    /// Nanosecond timestamp of last update.
    pub last_update_ns: u64,
}

impl Default for PoolAmm {
    fn default() -> Self {
        // SAFETY: all-zero is valid for every field
        unsafe { core::mem::zeroed() }
    }
}

impl PoolAmm {
    /// Reconstruct reserve0 as u128.
    #[inline(always)]
    pub fn reserve0(&self) -> u128 {
        (self.reserve0_hi as u128) << 64 | self.reserve0_lo as u128
    }

    /// Reconstruct reserve1 as u128.
    #[inline(always)]
    pub fn reserve1(&self) -> u128 {
        (self.reserve1_hi as u128) << 64 | self.reserve1_lo as u128
    }

    /// Whether this is a V2 (constant-product) pool.
    #[inline(always)]
    pub fn is_v2(&self) -> bool {
        self.pool_type == pool_type::V2
    }

    /// Whether this is a V3/CLMM (concentrated liquidity) pool.
    #[inline(always)]
    pub fn is_v3(&self) -> bool {
        self.pool_type == pool_type::V3_CLMM
    }

    /// Whether this state has been populated (non-zero block).
    #[inline(always)]
    pub fn is_valid(&self) -> bool {
        self.last_block > 0
    }

    /// Fee as basis points (e.g. 3000 ppm → 30 bps).
    #[inline(always)]
    pub fn fee_bps(&self) -> u32 {
        self.fee_ppm / 100
    }
}

/// Table of raw AMM states, parallel to PoolBooks.
///
/// Written to WASM memory at `POOL_STATE_TABLE_WASM_OFFSET` (0x23000).
/// Conditional write: skipped when `count == 0`.
#[derive(Clone, Copy)]
#[repr(C)]
pub struct PoolStateTable {
    /// Number of valid entries (matches `PoolBooks.pool_ct`).
    pub count: u8,
    /// Padding to align `states` at 16-byte boundary (u128 fields in PoolAmm).
    pub _pad: [u8; 15],
    /// Per-pool AMM states. `states[i]` corresponds to `PoolBooks.metas[i]`.
    pub states: [PoolAmm; MAX_POOL_STATES],
}

impl Default for PoolStateTable {
    fn default() -> Self {
        Self {
            count: 0,
            _pad: [0; 15],
            states: [PoolAmm::default(); MAX_POOL_STATES],
        }
    }
}

impl PoolStateTable {
    /// Get the AMM state for a pool slot.
    #[inline(always)]
    pub fn state_at(&self, slot: usize) -> &PoolAmm {
        if slot < self.count as usize {
            &self.states[slot]
        } else {
            &self.states[0]
        }
    }
}

// Compile-time ABI checks for PoolBooks
const _: () = assert!(
    POOL_BOOKS_WASM_OFFSET as usize >= crate::VENUE_BOOKS_WASM_OFFSET as usize,
    "POOL_BOOKS_WASM_OFFSET must not overlap VenueBooks"
);

// Compile-time ABI checks for PoolStateTable
const _: () = assert!(
    core::mem::size_of::<PoolAmm>() == 96,
    "PoolAmm must be exactly 96 bytes"
);
const _: () = assert!(
    core::mem::size_of::<PoolStateTable>() == 3088,
    "PoolStateTable must be exactly 3088 bytes (16 + 32*96)"
);
const _: () = assert!(
    POOL_STATE_TABLE_WASM_OFFSET as usize + core::mem::size_of::<PoolStateTable>() < 0x1000000,
    "PoolStateTable exceeds WASM 16MB memory limit"
);