sequence-algo-sdk 0.4.0

Sequence Markets Algo SDK — write HFT trading algos in Rust, compile to WASM, deploy to Sequence
Documentation
//! Online microstructure features and chain fee data.

// =============================================================================
// Online Features (Phase 2B — microstructure features delivered to algos)
// =============================================================================

/// WASM offset for OnlineFeatures — page-aligned after PoolBooks.
/// PoolBooks at 0x14000, size ~23KB -> ends well before 0x1A000.
pub const ONLINE_FEATURES_WASM_OFFSET: u32 = 0x1A000;

/// Online microstructure features delivered by the CC data engine.
/// Written to WASM memory before each algo callback.
/// Old algos that never read 0x1A000 are unaffected (backward compatible).
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct OnlineFeatures {
    /// ABI version (currently 1).
    pub version: u16,
    /// Bit flags: bit 0 = vpin_valid.
    pub flags: u16,
    pub _pad0: [u8; 4],

    // ── Microprice ───────────────────────────────────────────────────────
    /// Microprice scaled 1e9.
    pub microprice_1e9: u64,

    // ── OFI / MLOFI (scaled 1e8) ────────────────────────────────────────
    /// Order flow imbalance, top-of-book, scaled 1e8.
    pub ofi_1level_1e8: i64,
    /// Order flow imbalance, 5 levels, scaled 1e8.
    pub ofi_5level_1e8: i64,
    /// Multi-level OFI (10 levels), scaled 1e8.
    pub mlofi_10_1e8: i64,
    /// OFI EWMA, scaled 1e6.
    pub ofi_ewma_1e6: i64,

    // ── Trade flow ───────────────────────────────────────────────────────
    /// Trade sign imbalance [-1e6, +1e6].
    pub trade_sign_imbalance_1e6: i64,
    /// Trades/sec x 1000.
    pub trade_arrival_rate_1e3: u32,
    /// VPIN [0, 10000] — only valid when flags bit 0 is set.
    pub vpin_1e4: u16,
    pub _pad1: u16,

    // ── Spread state ─────────────────────────────────────────────────────
    /// 0=tight, 1=normal, 2=wide, 3=crisis.
    pub spread_regime: u8,
    pub _pad2a: u8,
    /// Spread z-score x 1000.
    pub spread_zscore_1e3: i16,

    // ── Depth analytics ──────────────────────────────────────────────────
    /// Cancel rate [0, 10000].
    pub cancel_rate_1e4: u16,
    /// Depth imbalance [-10000, +10000].
    pub depth_imbalance_1e4: i16,

    // ── Realized vol ─────────────────────────────────────────────────────
    /// 1-minute realized vol in basis points.
    pub rv_1m_bps: u32,
    /// 5-minute realized vol in basis points.
    pub rv_5m_bps: u32,
    /// 1-hour realized vol in basis points.
    pub rv_1h_bps: u32,
    pub _pad3: u32,

    // ── Multi-head prediction (populated by Phase 6 sidecar) ─────────────
    /// Probability of up direction [0, 10000].
    pub pred_dir_up_1e4: u16,
    /// Probability of flat direction [0, 10000].
    pub pred_dir_flat_1e4: u16,
    /// Probability of down direction [0, 10000].
    pub pred_dir_down_1e4: u16,
    /// Probability of normal stress [0, 10000].
    pub pred_stress_normal_1e4: u16,
    /// Probability of widening stress [0, 10000].
    pub pred_stress_widening_1e4: u16,
    /// Probability of crisis stress [0, 10000].
    pub pred_stress_crisis_1e4: u16,
    /// Probability of toxic flow [0, 10000].
    pub pred_toxic_1e4: u16,
    /// Age of prediction in ms.
    pub prediction_age_ms: u16,

    // ── Fill probability (populated by Phase 6 sidecar) ──────────────────
    /// Fill probability on bid side [0, 10000].
    pub fill_prob_bid_1e4: u16,
    /// Fill probability on ask side [0, 10000].
    pub fill_prob_ask_1e4: u16,
    /// Queue decay rate [0, 10000].
    pub queue_decay_rate_1e4: u16,
    pub _fill_pad: u16,

    // ── Timestamp ────────────────────────────────────────────────────────
    /// Feature computation timestamp in nanoseconds.
    pub feature_ts_ns: u64,

    /// Reserved for future expansion.
    pub _reserved: [u8; 136],
}

impl Default for OnlineFeatures {
    fn default() -> Self {
        // Safety: all-zero is valid for every field, then set version=1
        let mut f = unsafe { core::mem::zeroed::<Self>() };
        f.version = 1;
        f
    }
}

impl OnlineFeatures {
    /// Whether the VPIN field is valid (flags bit 0).
    #[inline(always)]
    pub fn vpin_valid(&self) -> bool {
        self.flags & 1 != 0
    }

    /// Microprice as f64 (divide by 1e9).
    #[inline(always)]
    pub fn microprice_f64(&self) -> f64 {
        self.microprice_1e9 as f64 / 1_000_000_000.0
    }

    /// OFI 1-level as f64 (divide by 1e8).
    #[inline(always)]
    pub fn ofi_1level_f64(&self) -> f64 {
        self.ofi_1level_1e8 as f64 / 100_000_000.0
    }

    /// Trade sign imbalance as f64 in [-1.0, +1.0].
    #[inline(always)]
    pub fn trade_sign_imbalance_f64(&self) -> f64 {
        self.trade_sign_imbalance_1e6 as f64 / 1_000_000.0
    }
}

// Compile-time ABI checks for OnlineFeatures
const _: () = assert!(
    core::mem::size_of::<OnlineFeatures>() == 256,
    "OnlineFeatures must be exactly 256 bytes"
);
const _: () = assert!(
    0x1A000 >= 0x14000 + core::mem::size_of::<crate::PoolBooks>(),
    "ONLINE_FEATURES_WASM_OFFSET overlaps with PoolBooks"
);
const _: () = assert!(
    ONLINE_FEATURES_WASM_OFFSET as usize + core::mem::size_of::<OnlineFeatures>() < 0x1000000,
    "OnlineFeatures exceeds WASM 16MB memory limit"
);

// =============================================================================
// CHAIN FEE TABLE (per-chain gas fee data for cost-aware algorithms)
// =============================================================================

/// WASM offset for ChainFeeTable — page-aligned after OnlineFeatures.
/// OnlineFeatures at 0x1A000 (256 bytes) ends at 0x1A100. Headroom to 0x1B000.
pub const CHAIN_FEE_TABLE_WASM_OFFSET: u32 = 0x1B000;

/// Maximum chains tracked in a ChainFeeTable.
pub const MAX_CHAINS: usize = 8;

/// Per-chain gas fee snapshot. Interpretation depends on chain_id:
/// - EVM (chain_id 0-4): base_fee_native=gwei, priority_fee_native=gwei,
///   estimated_gas_units=gas (150k-300k depending on protocol)
/// - Solana (chain_id 5): base_fee_native=lamports (5000), priority_fee_native=micro-lamports/CU,
///   estimated_gas_units=compute units (~200k)
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct ChainFee {
    /// Chain identifier (0=eth, 1=arb, 2=base, 3=op, 4=polygon, 5=solana).
    pub chain_id: u8,
    pub _pad: [u8; 7],
    /// Base fee in chain-native units.
    pub base_fee_native: u64,
    /// Priority/tip fee in chain-native units.
    pub priority_fee_native: u64,
    /// Estimated gas/compute units for a standard swap.
    pub estimated_gas_units: u64,
    /// Native token price in USD, 1e9-scaled (ETH/MATIC/SOL).
    pub native_price_1e9: u64,
    /// Timestamp of last observation (nanoseconds since epoch).
    pub last_update_ns: u64,
}

impl Default for ChainFee {
    fn default() -> Self {
        Self {
            chain_id: 255,
            _pad: [0; 7],
            base_fee_native: 0,
            priority_fee_native: 0,
            estimated_gas_units: 0,
            native_price_1e9: 0,
            last_update_ns: 0,
        }
    }
}

impl ChainFee {
    /// Total gas cost in the smallest native unit (gwei for EVM, lamports for Solana).
    ///
    /// - EVM:    (base_gwei + priority_gwei) * gas_units  → total gwei
    /// - Solana: base_lamports + (priority_micro_lamports * CU / 1_000_000) → total lamports
    pub fn total_gas_cost_native(&self) -> u64 {
        if self.chain_id == chain_id::SOLANA {
            let priority_lamports = (self.priority_fee_native as u128)
                .saturating_mul(self.estimated_gas_units as u128)
                / 1_000_000;
            (self.base_fee_native as u128).saturating_add(priority_lamports) as u64
        } else {
            (self.base_fee_native + self.priority_fee_native)
                .saturating_mul(self.estimated_gas_units)
        }
    }

    /// Estimated gas cost in USD (1e9 scaled).
    ///
    /// Both EVM and Solana share the same final step once `total_gas_cost_native()`
    /// returns the correct smallest-unit value:
    ///   cost_usd_1e9 = total_native * native_price_1e9 / 1e9
    /// which equals cost_usd = total_native * price / 1e18 (the /1e9 handles
    /// smallest-unit→whole-coin, the price is already 1e9-scaled).
    pub fn gas_cost_usd_1e9(&self) -> u64 {
        let cost = self.total_gas_cost_native();
        ((cost as u128 * self.native_price_1e9 as u128) / 1_000_000_000) as u64
    }
}

/// Per-symbol chain fee table. Written to WASM memory at CHAIN_FEE_TABLE_WASM_OFFSET.
/// Algos join pool→chain via `venue_chain_id(pool_meta.venue_id)`.
#[derive(Clone, Copy)]
#[repr(C)]
pub struct ChainFeeTable {
    /// Number of valid entries in `chains[]`.
    pub chain_ct: u8,
    pub _pad: [u8; 7],
    /// Per-chain fee snapshots.
    pub chains: [ChainFee; MAX_CHAINS],
}

impl Default for ChainFeeTable {
    fn default() -> Self {
        Self {
            chain_ct: 0,
            _pad: [0; 7],
            chains: [ChainFee::default(); MAX_CHAINS],
        }
    }
}

impl ChainFeeTable {
    /// Look up fee data for a specific chain.
    pub fn fee_for_chain(&self, chain_id: u8) -> Option<&ChainFee> {
        for i in 0..self.chain_ct as usize {
            if i < MAX_CHAINS && self.chains[i].chain_id == chain_id {
                return Some(&self.chains[i]);
            }
        }
        None
    }
}

/// Chain ID constants for use with `ChainFeeTable::fee_for_chain()`.
pub mod chain_id {
    pub const ETHEREUM: u8 = 0;
    pub const ARBITRUM: u8 = 1;
    pub const BASE: u8 = 2;
    pub const OPTIMISM: u8 = 3;
    pub const POLYGON: u8 = 4;
    pub const SOLANA: u8 = 5;
}

/// Map venue_id (from PoolMeta/NbboSnapshot) to chain_id.
pub fn venue_chain_id(venue_id: u8) -> u8 {
    match venue_id {
        10 => chain_id::ETHEREUM, // VENUE_DEX_ETH
        11 => chain_id::ARBITRUM, // VENUE_DEX_ARB
        12 => chain_id::BASE,     // VENUE_DEX_BASE
        13 => chain_id::OPTIMISM, // VENUE_DEX_OP
        14 => chain_id::POLYGON,  // VENUE_DEX_POLY
        15 => chain_id::SOLANA,   // VENUE_DEX_SOL
        _ => 255,                  // Unknown / CEX
    }
}

// Compile-time ABI checks for ChainFeeTable
const _: () = assert!(
    core::mem::size_of::<ChainFee>() == 48,
    "ChainFee must be exactly 48 bytes"
);
const _: () = assert!(
    core::mem::size_of::<ChainFeeTable>() == 392,
    "ChainFeeTable must be exactly 392 bytes (8 + 48*8)"
);
const _: () = assert!(
    CHAIN_FEE_TABLE_WASM_OFFSET as usize
        >= ONLINE_FEATURES_WASM_OFFSET as usize + core::mem::size_of::<OnlineFeatures>(),
    "ChainFeeTable overlaps OnlineFeatures"
);