sequence-algo-sdk 0.4.0

Sequence Markets Algo SDK — write HFT trading algos in Rust, compile to WASM, deploy to Sequence
Documentation
//! Type-safe fixed-point units — prevents mixing prices and quantities.
//!
//! `Px` wraps a 1e9-scaled price, `Qty` wraps a 1e8-scaled quantity.
//! Both are `repr(transparent)` — zero-cost at runtime, compile-time safety.
//!
//! # Example
//! ```rust,ignore
//! use algo_sdk::units::{Px, Qty};
//!
//! let price = Px::from_float(50_000.50);  // $50,000.50
//! let qty = Qty::from_float(1.5);          // 1.5 BTC
//! let notional = price.notional(qty);      // $75,000.75
//! ```

/// Price in 1e9 fixed-point ($1.00 = 1_000_000_000).
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(transparent)]
pub struct Px(pub i64);

/// Quantity in 1e8 fixed-point (1.0 = 100_000_000).
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(transparent)]
pub struct Qty(pub i64);

// ── Px ──────────────────────────────────────────────────────────────────

impl Px {
    pub const ZERO: Self = Self(0);

    /// Create from raw 1e9-scaled value.
    #[inline(always)]
    pub const fn raw(v: i64) -> Self { Self(v) }

    /// Create from unsigned raw (for book prices which are u64).
    #[inline(always)]
    pub const fn from_u64(v: u64) -> Self { Self(v as i64) }

    /// Create from float (convenience, not for hot path).
    #[inline]
    pub fn from_float(v: f64) -> Self { Self((v * 1_000_000_000.0) as i64) }

    /// Convert to float.
    #[inline(always)]
    pub fn to_float(self) -> f64 { self.0 as f64 / 1_000_000_000.0 }

    /// Raw i64 value.
    #[inline(always)]
    pub const fn as_raw(self) -> i64 { self.0 }

    /// Raw u64 value (for book prices).
    #[inline(always)]
    pub const fn as_u64(self) -> u64 { self.0 as u64 }

    /// Midpoint between two prices.
    #[inline(always)]
    pub fn mid(self, other: Px) -> Px { Px((self.0 + other.0) / 2) }

    /// Spread in basis points (self = ask, other = bid).
    #[inline(always)]
    pub fn spread_bps(self, bid: Px) -> i32 {
        let mid = (self.0 + bid.0) / 2;
        if mid == 0 { return i32::MAX; }
        (((self.0 - bid.0) as i128 * 10_000) / mid as i128) as i32
    }

    /// Compute notional value: price * qty.
    /// Returns value in 1e9 (USD-scaled).
    #[inline(always)]
    pub fn notional(self, qty: Qty) -> i64 {
        ((self.0 as i128 * qty.0 as i128) / 100_000_000) as i64
    }

    /// Offset by basis points. Positive = higher price.
    #[inline(always)]
    pub fn offset_bps(self, bps: i32) -> Px {
        Px(self.0 + ((self.0 as i128 * bps as i128) / 10_000) as i64)
    }
}

// ── Qty ─────────────────────────────────────────────────────────────────

impl Qty {
    pub const ZERO: Self = Self(0);

    /// Create from raw 1e8-scaled value.
    #[inline(always)]
    pub const fn raw(v: i64) -> Self { Self(v) }

    /// Create from float (convenience, not for hot path).
    #[inline]
    pub fn from_float(v: f64) -> Self { Self((v * 100_000_000.0) as i64) }

    /// Convert to float.
    #[inline(always)]
    pub fn to_float(self) -> f64 { self.0 as f64 / 100_000_000.0 }

    /// Raw i64 value.
    #[inline(always)]
    pub const fn as_raw(self) -> i64 { self.0 }

    /// Absolute value.
    #[inline(always)]
    pub fn abs(self) -> Qty { Qty(self.0.abs()) }

    /// Whether this is zero.
    #[inline(always)]
    pub fn is_zero(self) -> bool { self.0 == 0 }

    /// Whether this is a buy (positive).
    #[inline(always)]
    pub fn is_long(self) -> bool { self.0 > 0 }

    /// Whether this is a sell (negative).
    #[inline(always)]
    pub fn is_short(self) -> bool { self.0 < 0 }

    /// Split into N equal parts (for laddering).
    #[inline]
    pub fn split(self, n: u32) -> Qty {
        if n == 0 { return self; }
        Qty(self.0 / n as i64)
    }
}

// ── Display / Debug ─────────────────────────────────────────────────────

impl core::fmt::Debug for Px {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "Px(${:.4})", self.to_float())
    }
}

impl core::fmt::Display for Px {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "${:.4}", self.to_float())
    }
}

impl core::fmt::Debug for Qty {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "Qty({:.8})", self.to_float())
    }
}

impl core::fmt::Display for Qty {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{:.8}", self.to_float())
    }
}

// ── From / Into raw ─────────────────────────────────────────────────────

impl From<i64> for Px {
    #[inline(always)]
    fn from(v: i64) -> Self { Self(v) }
}
impl From<u64> for Px {
    #[inline(always)]
    fn from(v: u64) -> Self { Self(v as i64) }
}
impl From<Px> for i64 {
    #[inline(always)]
    fn from(v: Px) -> Self { v.0 }
}
impl From<Px> for u64 {
    #[inline(always)]
    fn from(v: Px) -> Self { v.0 as u64 }
}

impl From<i64> for Qty {
    #[inline(always)]
    fn from(v: i64) -> Self { Self(v) }
}
impl From<Qty> for i64 {
    #[inline(always)]
    fn from(v: Qty) -> Self { v.0 }
}

// ── Arithmetic ──────────────────────────────────────────────────────────

impl core::ops::Add for Px {
    type Output = Px;
    #[inline(always)]
    fn add(self, rhs: Px) -> Px { Px(self.0 + rhs.0) }
}

impl core::ops::Sub for Px {
    type Output = Px;
    #[inline(always)]
    fn sub(self, rhs: Px) -> Px { Px(self.0 - rhs.0) }
}

impl core::ops::Neg for Px {
    type Output = Px;
    #[inline(always)]
    fn neg(self) -> Px { Px(-self.0) }
}

impl core::ops::Add for Qty {
    type Output = Qty;
    #[inline(always)]
    fn add(self, rhs: Qty) -> Qty { Qty(self.0 + rhs.0) }
}

impl core::ops::Sub for Qty {
    type Output = Qty;
    #[inline(always)]
    fn sub(self, rhs: Qty) -> Qty { Qty(self.0 - rhs.0) }
}

impl core::ops::Neg for Qty {
    type Output = Qty;
    #[inline(always)]
    fn neg(self) -> Qty { Qty(-self.0) }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn px_from_float() {
        let p = Px::from_float(50_000.50);
        assert_eq!(p.0, 50_000_500_000_000);
        assert!((p.to_float() - 50_000.50).abs() < 0.001);
    }

    #[test]
    fn qty_from_float() {
        let q = Qty::from_float(1.5);
        assert_eq!(q.0, 150_000_000);
        assert!((q.to_float() - 1.5).abs() < 0.001);
    }

    #[test]
    fn px_notional() {
        let px = Px::from_float(50_000.0);
        let qty = Qty::from_float(2.0);
        let notional = px.notional(qty);
        // 100_000 USD in 1e9 = 100_000_000_000_000
        assert_eq!(notional, 100_000_000_000_000);
    }

    #[test]
    fn px_spread_bps() {
        let ask = Px::raw(101_000_000_000);
        let bid = Px::raw(99_000_000_000);
        assert_eq!(ask.spread_bps(bid), 200);
    }

    #[test]
    fn px_offset_bps() {
        let p = Px::raw(100_000_000_000); // $100
        let up = p.offset_bps(100); // +1%
        assert_eq!(up.0, 101_000_000_000);
    }

    #[test]
    fn qty_split() {
        let q = Qty::from_float(10.0);
        let part = q.split(5);
        assert_eq!(part, Qty::from_float(2.0));
    }

    #[test]
    fn zero_cost_repr() {
        assert_eq!(core::mem::size_of::<Px>(), core::mem::size_of::<i64>());
        assert_eq!(core::mem::size_of::<Qty>(), core::mem::size_of::<i64>());
    }
}