#![cfg_attr(not(feature = "std"), no_std)]
#![allow(non_snake_case)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std as alloc;
pub mod book;
pub mod state;
pub mod events;
pub mod actions;
pub mod venue;
pub mod pool;
pub mod features;
pub mod traits;
#[doc(hidden)]
pub mod wasm_macros;
pub mod messaging;
pub mod amm_math;
pub mod log;
pub mod units;
pub mod config;
pub mod testing;
pub use book::{Level, L2Book};
pub use state::{
AlgoState, OpenOrder, PnlSnapshot, RiskSnapshot, SymbolMeta, Status, MAX_ORDERS,
};
pub use events::{Fill, FillExt, Reject, RejectCode};
pub use actions::{
Action, Actions, WasmActions, OrderType,
MAX_ACTIONS, ACTION_NEW, ACTION_CANCEL, ACTION_AMEND,
};
pub use venue::{
NbboSnapshot, VenueBooks,
MAX_VENUES, VENUE_BOOKS_WASM_OFFSET,
VENUE_KRAKEN, VENUE_COINBASE, VENUE_BINANCE, VENUE_BITGET,
VENUE_CRYPTOCOM, VENUE_BITMART, VENUE_DEX, VENUE_OKX, VENUE_BYBIT,
VENUE_UNKNOWN, VENUE_DEX_ETH, VENUE_DEX_ARB, VENUE_DEX_BASE,
VENUE_DEX_OP, VENUE_DEX_POLY, VENUE_DEX_SOL, VENUE_HYPERLIQUID,
is_dex, is_cex, venue_name,
};
pub use pool::{
PoolMeta, PoolBooks, PoolAmm, PoolStateTable, pool_type,
MAX_POOLS, MAX_POOL_STATES,
POOL_BOOKS_WASM_OFFSET, POOL_STATE_TABLE_WASM_OFFSET,
};
pub use features::{
OnlineFeatures, ChainFee, ChainFeeTable, chain_id, venue_chain_id,
ONLINE_FEATURES_WASM_OFFSET, CHAIN_FEE_TABLE_WASM_OFFSET, MAX_CHAINS,
};
pub use traits::Algo;
pub use messaging::{send, MAX_MESSAGE_SIZE};
pub use actions::{ACTION_INTENT, IntentPolicy};
pub use log::LogLevel;
pub use config::{ConfigRegion, CONFIG_REGION_WASM_OFFSET, MAX_CONFIG_PARAMS};
pub mod time {
#[inline(always)]
pub fn start(now_ns: u64) -> u64 { now_ns }
#[inline(always)]
pub fn stop_ns(start_ns: u64, now_ns: u64) -> u64 {
now_ns.saturating_sub(start_ns)
}
#[inline(always)]
pub fn stop_us(start_ns: u64, now_ns: u64) -> u64 {
stop_ns(start_ns, now_ns) / 1_000
}
#[inline(always)]
pub fn stop_ms(start_ns: u64, now_ns: u64) -> u64 {
stop_ns(start_ns, now_ns) / 1_000_000
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Timer { start_ns: u64 }
impl Timer {
#[inline(always)]
pub const fn new() -> Self { Self { start_ns: 0 } }
#[inline(always)]
pub fn start(&mut self, now_ns: u64) { self.start_ns = now_ns; }
#[inline(always)]
pub fn stop_ns(&self, now_ns: u64) -> u64 {
now_ns.saturating_sub(self.start_ns)
}
#[inline(always)]
pub fn stop_us(&self, now_ns: u64) -> u64 { self.stop_ns(now_ns) / 1_000 }
#[inline(always)]
pub fn stop_ms(&self, now_ns: u64) -> u64 { self.stop_ns(now_ns) / 1_000_000 }
}
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {
$crate::log::info_fmt(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {
$crate::log::warn_fmt(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {
$crate::log::error_fmt(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {
$crate::log::debug_fmt(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! sequence_algo_entry {
() => {
#[cfg(target_arch = "wasm32")]
mod _seq_entry {
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}
struct BumpAlloc;
static mut HEAP_POS: usize = 0;
static mut HEAP: [u8; 65536] = [0u8; 65536];
unsafe impl core::alloc::GlobalAlloc for BumpAlloc {
unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
let align = layout.align();
let pos = (HEAP_POS + align - 1) & !(align - 1);
let end = pos + layout.size();
if end > HEAP.len() {
return core::ptr::null_mut();
}
HEAP_POS = end;
HEAP.as_mut_ptr().add(pos)
}
unsafe fn dealloc(&self, _: *mut u8, _: core::alloc::Layout) {}
}
#[global_allocator]
static ALLOC: BumpAlloc = BumpAlloc;
}
};
}
#[cfg(test)]
mod tests {
use super::*;
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
}
#[test]
fn level_empty_is_zero() {
let l = Level::EMPTY;
assert_eq!(l.px_1e9, 0);
assert_eq!(l.sz_1e8, 0);
}
#[test]
fn level_is_valid() {
assert!(!Level::EMPTY.is_valid());
assert!(Level { px_1e9: 1, sz_1e8: 0 }.is_valid());
}
#[test]
fn book_best_bid_ask_empty() {
let book = L2Book::default();
assert!(book.best_bid().is_none());
assert!(book.best_ask().is_none());
}
#[test]
fn book_best_bid_ask_populated() {
let book = make_book(99_000_000_000, 1_000_000, 101_000_000_000, 2_000_000);
assert_eq!(book.best_bid().unwrap().px_1e9, 99_000_000_000);
assert_eq!(book.best_ask().unwrap().px_1e9, 101_000_000_000);
}
#[test]
fn book_mid_and_spread() {
let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
assert_eq!(book.mid_px_1e9(), 100_000_000_000);
assert_eq!(book.spread_1e9(), 2_000_000_000);
assert_eq!(book.spread_bps(), 200);
assert_eq!(book.spread_bps_x1000(), 200_000);
assert!(!book.is_crossed());
}
#[test]
fn book_crossed() {
let book = make_book(101_000_000_000, 1, 99_000_000_000, 1);
assert!(book.is_crossed());
assert!(book.spread_bps_x1000() < 0);
}
#[test]
fn book_depth_and_imbalance() {
let mut book = make_book(100_000_000_000, 500, 101_000_000_000, 300);
book.bids[1] = Level { px_1e9: 99_000_000_000, sz_1e8: 200 };
book.bid_ct = 2;
assert_eq!(book.bid_depth_1e8(5), 700);
assert_eq!(book.imbalance_bps(5), 4000);
}
#[test]
fn open_order_lifecycle() {
let mut o = OpenOrder::EMPTY;
o.status = Status::ACKED;
assert!(o.is_live());
o.status = Status::DEAD;
assert!(!o.is_live());
}
#[test]
fn symbol_rounding() {
let meta = SymbolMeta {
tick_size_1e9: 10_000_000,
lot_size_1e8: 1_000_000,
..SymbolMeta::EMPTY
};
assert_eq!(meta.round_px(95_000_000), 90_000_000);
assert_eq!(meta.round_qty(1_500_000), 1_000_000);
}
#[test]
fn algo_state_pnl() {
let mut s = AlgoState::default();
s.realized_pnl_1e9 = 5_000_000_000;
s.unrealized_pnl_1e9 = -2_000_000_000;
assert_eq!(s.total_pnl_1e9(), 3_000_000_000);
assert!((s.total_pnl_usd() - 3.0).abs() < 1e-9);
}
#[test]
fn fill_timing() {
let fill = Fill { order_id: 1, px_1e9: 0, qty_1e8: 0, recv_ns: 10_000_000, side: 1, _pad: [0; 7] };
assert_eq!(fill.since_ms(5_000_000), 5);
}
#[test]
fn reject_reason() {
let r = Reject { order_id: 1, code: RejectCode::AUTH, _pad: [0; 7] };
assert_eq!(r.reason(), "AUTH");
}
#[test]
fn actions_buy_sell_cancel() {
let mut a = Actions::new();
assert!(a.buy(1, 100, 50_000));
assert!(a.sell(2, 200, 60_000));
assert!(a.cancel(3));
assert_eq!(a.len(), 3);
assert_eq!(a.get(0).unwrap().side, 1);
assert_eq!(a.get(1).unwrap().side, -1);
assert_eq!(a.get(2).unwrap().is_cancel, 1);
}
#[test]
fn actions_order_types() {
let mut a = Actions::new();
a.ioc_buy(1, 10, 100);
a.fok_buy(2, 20, 200);
a.post_only_buy(3, 30, 300);
a.market_buy(4, 40);
assert_eq!(a.get(0).unwrap().order_type, OrderType::IOC);
assert_eq!(a.get(1).unwrap().order_type, OrderType::FOK);
assert_eq!(a.get(2).unwrap().order_type, OrderType::POST_ONLY);
assert_eq!(a.get(3).unwrap().order_type, OrderType::MARKET);
}
#[test]
fn venue_classification() {
assert!(is_cex(VENUE_KRAKEN));
assert!(is_cex(VENUE_COINBASE));
assert!(is_cex(VENUE_HYPERLIQUID));
assert!(is_dex(VENUE_DEX));
assert!(is_dex(VENUE_DEX_SOL));
assert!(!is_cex(VENUE_DEX));
assert!(!is_dex(VENUE_KRAKEN));
}
#[test]
fn venue_names() {
assert_eq!(venue_name(VENUE_KRAKEN), "kraken");
assert_eq!(venue_name(VENUE_DEX_SOL), "dex-sol");
assert_eq!(venue_name(0), "default");
}
#[test]
fn nbbo_spread_and_crossed() {
let mut snap = NbboSnapshot::default();
snap.nbbo_bid_px_1e9 = 99_000_000_000;
snap.nbbo_ask_px_1e9 = 101_000_000_000;
snap.nbbo_bid_venue = 0;
snap.nbbo_ask_venue = 1;
snap.venue_ct = 2;
assert!(!snap.is_crossed());
snap.nbbo_bid_px_1e9 = 101_500_000_000;
assert!(snap.is_crossed());
}
#[test]
fn abi_sizes_unchanged() {
assert_eq!(core::mem::size_of::<L2Book>(), 656);
assert_eq!(core::mem::size_of::<Fill>(), 40);
assert_eq!(core::mem::size_of::<Reject>(), 16);
assert_eq!(core::mem::size_of::<Action>(), 32);
assert_eq!(core::mem::size_of::<OnlineFeatures>(), 256);
}
#[test]
fn venue_books_lookup() {
let mut vb = VenueBooks::default();
vb.book_ct = 2;
vb.venue_ids[0] = VENUE_KRAKEN;
vb.venue_ids[1] = VENUE_COINBASE;
vb.books[0].bid_ct = 1;
vb.books[0].bids[0].px_1e9 = 100_000_000_000;
assert_eq!(vb.book_for_venue(VENUE_KRAKEN).unwrap().bids[0].px_1e9, 100_000_000_000);
assert!(vb.book_for_venue(VENUE_BINANCE).is_none());
}
#[test]
fn time_helpers() {
assert_eq!(time::stop_ns(1000, 2500), 1500);
assert_eq!(time::stop_us(1000, 2_001_000), 2_000);
let mut t = time::Timer::new();
t.start(1_000_000);
assert_eq!(t.stop_ns(3_000_000), 2_000_000);
}
}