1#![cfg_attr(not(feature = "std"), no_std)]
27#![allow(non_snake_case)]
28
29#[cfg(not(feature = "std"))]
30extern crate alloc;
31
32#[cfg(feature = "std")]
33extern crate std as alloc;
34
35#[derive(Debug, Clone, Copy, Default)]
41#[repr(C)]
42pub struct Level {
43 pub px_1e9: u64, pub sz_1e8: u64, }
46
47impl Level {
48 pub const EMPTY: Self = Self {
49 px_1e9: 0,
50 sz_1e8: 0,
51 };
52
53 #[inline(always)]
54 pub fn is_valid(&self) -> bool {
55 self.px_1e9 > 0
56 }
57}
58
59#[derive(Clone, Copy)]
66#[repr(C)]
67pub struct L2Book {
68 pub bids: [Level; 20], pub asks: [Level; 20], pub bid_ct: u8, pub ask_ct: u8, pub symbol_id: u16,
73 pub _pad: u32,
74 pub recv_ns: u64, }
76
77impl Default for L2Book {
78 fn default() -> Self {
79 Self {
80 bids: [Level::EMPTY; 20],
81 asks: [Level::EMPTY; 20],
82 bid_ct: 0,
83 ask_ct: 0,
84 symbol_id: 0,
85 _pad: 0,
86 recv_ns: 0,
87 }
88 }
89}
90
91impl L2Book {
92 #[inline(always)]
93 pub fn best_bid(&self) -> Option<&Level> {
94 if self.bid_ct > 0 && self.bids[0].px_1e9 > 0 {
95 Some(&self.bids[0])
96 } else {
97 None
98 }
99 }
100
101 #[inline(always)]
102 pub fn best_ask(&self) -> Option<&Level> {
103 if self.ask_ct > 0 && self.asks[0].px_1e9 > 0 {
104 Some(&self.asks[0])
105 } else {
106 None
107 }
108 }
109
110 #[inline(always)]
111 pub fn mid_px_1e9(&self) -> u64 {
112 if self.bid_ct == 0 || self.ask_ct == 0 {
113 return 0;
114 }
115 (self.bids[0].px_1e9 + self.asks[0].px_1e9) / 2
116 }
117
118 #[inline(always)]
122 pub fn spread_1e9(&self) -> u64 {
123 if self.bid_ct == 0 || self.ask_ct == 0 {
124 return u64::MAX;
125 }
126 self.asks[0].px_1e9.saturating_sub(self.bids[0].px_1e9)
127 }
128
129 #[inline(always)]
131 pub fn spread_signed_1e9(&self) -> i64 {
132 if self.bid_ct == 0 || self.ask_ct == 0 {
133 return i64::MAX;
134 }
135 self.asks[0].px_1e9 as i64 - self.bids[0].px_1e9 as i64
136 }
137
138 #[inline(always)]
142 pub fn spread_bps(&self) -> u32 {
143 let mid = self.mid_px_1e9();
144 if mid == 0 {
145 return u32::MAX;
146 }
147 ((self.spread_1e9() * 10_000) / mid) as u32
148 }
149
150 #[inline(always)]
154 pub fn spread_bps_x1000(&self) -> i32 {
155 let mid = self.mid_px_1e9();
156 if mid == 0 {
157 return i32::MAX;
158 }
159 let spread = self.asks[0].px_1e9 as i128 - self.bids[0].px_1e9 as i128;
160 ((spread * 10_000_000) / mid as i128) as i32
161 }
162
163 #[inline(always)]
166 pub fn is_crossed(&self) -> bool {
167 self.bid_ct > 0
168 && self.ask_ct > 0
169 && self.bids[0].px_1e9 > self.asks[0].px_1e9
170 }
171
172 #[inline(always)]
173 pub fn bid_depth_1e8(&self, levels: usize) -> u64 {
174 let n = levels.min(self.bid_ct as usize);
175 let mut sum = 0u64;
176 for i in 0..n {
177 sum += self.bids[i].sz_1e8;
178 }
179 sum
180 }
181
182 #[inline(always)]
183 pub fn ask_depth_1e8(&self, levels: usize) -> u64 {
184 let n = levels.min(self.ask_ct as usize);
185 let mut sum = 0u64;
186 for i in 0..n {
187 sum += self.asks[i].sz_1e8;
188 }
189 sum
190 }
191
192 #[inline(always)]
193 pub fn imbalance_bps(&self, levels: usize) -> i32 {
194 let bid_depth = self.bid_depth_1e8(levels);
195 let ask_depth = self.ask_depth_1e8(levels);
196 let total = bid_depth + ask_depth;
197 if total == 0 {
198 return 0;
199 }
200 (((bid_depth as i64 - ask_depth as i64) * 10_000) / total as i64) as i32
201 }
202}
203
204pub mod Status {
210 pub const PENDING: u8 = 0; pub const ACKED: u8 = 1; pub const PARTIAL: u8 = 2; pub const DEAD: u8 = 3; }
215
216#[derive(Debug, Clone, Copy, Default)]
218#[repr(C)]
219pub struct OpenOrder {
220 pub order_id: u64,
221 pub px_1e9: u64,
222 pub qty_1e8: i64, pub filled_1e8: i64, pub side: i8, pub status: u8,
226 pub _pad: [u8; 6],
227}
228
229impl OpenOrder {
230 pub const EMPTY: Self = Self {
231 order_id: 0,
232 px_1e9: 0,
233 qty_1e8: 0,
234 filled_1e8: 0,
235 side: 0,
236 status: 0,
237 _pad: [0; 6],
238 };
239
240 #[inline(always)]
241 pub fn is_live(&self) -> bool {
242 self.status == Status::ACKED || self.status == Status::PARTIAL
243 }
244
245 #[inline(always)]
246 pub fn is_pending(&self) -> bool {
247 self.status == Status::PENDING
248 }
249
250 #[inline(always)]
251 pub fn remaining_1e8(&self) -> i64 {
252 self.qty_1e8.abs() - self.filled_1e8.abs()
253 }
254}
255
256#[derive(Debug, Clone, Copy)]
264#[repr(C)]
265pub struct SymbolMeta {
266 pub tick_size_1e9: u64,
268 pub lot_size_1e8: u64,
270 pub min_qty_1e8: u64,
272 pub min_notional_1e9: u64,
274 pub price_precision: u8,
276 pub qty_precision: u8,
278 pub _pad: [u8; 6],
279}
280
281impl Default for SymbolMeta {
282 fn default() -> Self {
283 Self {
284 tick_size_1e9: 0,
285 lot_size_1e8: 0,
286 min_qty_1e8: 0,
287 min_notional_1e9: 0,
288 price_precision: 0,
289 qty_precision: 0,
290 _pad: [0; 6],
291 }
292 }
293}
294
295impl SymbolMeta {
296 pub const EMPTY: Self = Self {
297 tick_size_1e9: 0,
298 lot_size_1e8: 0,
299 min_qty_1e8: 0,
300 min_notional_1e9: 0,
301 price_precision: 0,
302 qty_precision: 0,
303 _pad: [0; 6],
304 };
305
306 #[inline(always)]
308 pub fn round_px(&self, px_1e9: u64) -> u64 {
309 if self.tick_size_1e9 == 0 {
310 return px_1e9;
311 }
312 (px_1e9 / self.tick_size_1e9) * self.tick_size_1e9
313 }
314
315 #[inline(always)]
317 pub fn round_qty(&self, qty_1e8: i64) -> i64 {
318 if self.lot_size_1e8 == 0 {
319 return qty_1e8;
320 }
321 let lot = self.lot_size_1e8 as i64;
322 (qty_1e8 / lot) * lot
323 }
324
325 #[inline(always)]
327 pub fn check_notional(&self, qty_1e8: i64, px_1e9: u64) -> bool {
328 if self.min_notional_1e9 == 0 {
329 return true;
330 }
331 let notional = (qty_1e8.unsigned_abs() as u128 * px_1e9 as u128 / 100_000_000) as u64;
332 notional >= self.min_notional_1e9
333 }
334
335 #[inline(always)]
337 pub fn check_min_qty(&self, qty_1e8: i64) -> bool {
338 if self.min_qty_1e8 == 0 {
339 return true;
340 }
341 qty_1e8.unsigned_abs() >= self.min_qty_1e8
342 }
343}
344
345#[derive(Debug, Clone, Copy)]
352#[repr(C)]
353pub struct RiskSnapshot {
354 pub max_position_1e8: i64,
356 pub max_daily_loss_1e9: i64,
358 pub max_order_notional_1e9: u64,
360 pub max_order_rate: u32,
362 pub reduce_only: u8,
364 pub _pad: [u8; 3],
365}
366
367impl Default for RiskSnapshot {
368 fn default() -> Self {
369 Self {
370 max_position_1e8: 0,
371 max_daily_loss_1e9: 0,
372 max_order_notional_1e9: 0,
373 max_order_rate: 0,
374 reduce_only: 0,
375 _pad: [0; 3],
376 }
377 }
378}
379
380impl RiskSnapshot {
381 pub const EMPTY: Self = Self {
382 max_position_1e8: 0,
383 max_daily_loss_1e9: 0,
384 max_order_notional_1e9: 0,
385 max_order_rate: 0,
386 reduce_only: 0,
387 _pad: [0; 3],
388 };
389
390 #[inline(always)]
393 pub fn check_position(&self, current_position_1e8: i64, delta_1e8: i64) -> bool {
394 if self.max_position_1e8 == 0 {
395 return true;
396 }
397 let projected = current_position_1e8.saturating_add(delta_1e8);
398 projected.abs() <= self.max_position_1e8
399 }
400}
401
402pub const MAX_ORDERS: usize = 32;
408
409#[derive(Clone, Copy)]
415#[repr(C)]
416pub struct AlgoState {
417 pub position_1e8: i64, pub avg_entry_1e9: u64, pub realized_pnl_1e9: i64, pub unrealized_pnl_1e9: i64, pub orders: [OpenOrder; MAX_ORDERS],
424 pub order_ct: u8,
425 pub _pad: [u8; 7],
426 pub session_pnl_1e9: i64, pub total_fill_count: u64, pub symbol: SymbolMeta, pub risk: RiskSnapshot, }
434
435impl Default for AlgoState {
436 fn default() -> Self {
437 Self {
438 position_1e8: 0,
439 avg_entry_1e9: 0,
440 realized_pnl_1e9: 0,
441 unrealized_pnl_1e9: 0,
442 orders: [OpenOrder::EMPTY; MAX_ORDERS],
443 order_ct: 0,
444 _pad: [0; 7],
445 session_pnl_1e9: 0,
446 total_fill_count: 0,
447 symbol: SymbolMeta::EMPTY,
448 risk: RiskSnapshot::EMPTY,
449 }
450 }
451}
452
453#[derive(Debug, Clone, Copy, Default)]
455pub struct PnlSnapshot {
456 pub realized_1e9: i64,
457 pub unrealized_1e9: i64,
458 pub total_1e9: i64,
459}
460
461impl AlgoState {
462 #[inline(always)]
463 pub fn is_flat(&self) -> bool {
464 self.position_1e8 == 0
465 }
466
467 #[inline(always)]
468 pub fn is_long(&self) -> bool {
469 self.position_1e8 > 0
470 }
471
472 #[inline(always)]
473 pub fn is_short(&self) -> bool {
474 self.position_1e8 < 0
475 }
476
477 #[inline(always)]
478 pub fn has_orders(&self) -> bool {
479 self.order_ct > 0
480 }
481
482 #[inline(always)]
483 pub fn live_order_count(&self) -> usize {
484 let mut ct = 0;
485 for i in 0..self.order_ct as usize {
486 if self.orders[i].is_live() {
487 ct += 1;
488 }
489 }
490 ct
491 }
492
493 #[inline(always)]
494 pub fn find_order(&self, order_id: u64) -> Option<&OpenOrder> {
495 for i in 0..self.order_ct as usize {
496 if self.orders[i].order_id == order_id {
497 return Some(&self.orders[i]);
498 }
499 }
500 None
501 }
502
503 #[inline(always)]
504 pub fn open_buy_qty_1e8(&self) -> i64 {
505 let mut sum = 0i64;
506 for i in 0..self.order_ct as usize {
507 let o = &self.orders[i];
508 if o.is_live() && o.side > 0 {
509 sum += o.remaining_1e8();
510 }
511 }
512 sum
513 }
514
515 #[inline(always)]
516 pub fn open_sell_qty_1e8(&self) -> i64 {
517 let mut sum = 0i64;
518 for i in 0..self.order_ct as usize {
519 let o = &self.orders[i];
520 if o.is_live() && o.side < 0 {
521 sum += o.remaining_1e8();
522 }
523 }
524 sum
525 }
526
527 #[inline(always)]
528 pub fn total_pnl_1e9(&self) -> i64 {
529 self.realized_pnl_1e9 + self.unrealized_pnl_1e9
530 }
531
532 #[inline(always)]
534 pub fn get_pnl(&self) -> PnlSnapshot {
535 PnlSnapshot {
536 realized_1e9: self.realized_pnl_1e9,
537 unrealized_1e9: self.unrealized_pnl_1e9,
538 total_1e9: self.total_pnl_1e9(),
539 }
540 }
541
542 #[inline(always)]
544 pub fn realized_pnl_usd(&self) -> f64 {
545 self.realized_pnl_1e9 as f64 / 1e9
546 }
547
548 #[inline(always)]
550 pub fn unrealized_pnl_usd(&self) -> f64 {
551 self.unrealized_pnl_1e9 as f64 / 1e9
552 }
553
554 #[inline(always)]
556 pub fn total_pnl_usd(&self) -> f64 {
557 self.total_pnl_1e9() as f64 / 1e9
558 }
559
560 #[inline(always)]
562 pub fn session_pnl_usd(&self) -> f64 {
563 self.session_pnl_1e9 as f64 / 1e9
564 }
565}
566
567#[derive(Debug, Clone, Copy)]
572#[repr(C)]
573pub struct Fill {
574 pub order_id: u64,
575 pub px_1e9: u64,
576 pub qty_1e8: i64,
577 pub recv_ns: u64, pub side: i8,
579 pub _pad: [u8; 7],
580}
581
582impl Fill {
583 #[inline(always)]
586 pub fn since_ns(&self, start_ns: u64) -> u64 {
587 self.recv_ns.saturating_sub(start_ns)
588 }
589
590 #[inline(always)]
591 pub fn since_us(&self, start_ns: u64) -> u64 {
592 self.since_ns(start_ns) / 1_000
593 }
594
595 #[inline(always)]
596 pub fn since_ms(&self, start_ns: u64) -> u64 {
597 self.since_ns(start_ns) / 1_000_000
598 }
599
600 #[inline(always)]
604 pub fn symbol_id(&self) -> u16 {
605 u16::from_le_bytes([self._pad[0], self._pad[1]])
606 }
607
608 #[inline(always)]
610 pub fn set_symbol_id(&mut self, id: u16) {
611 let bytes = id.to_le_bytes();
612 self._pad[0] = bytes[0];
613 self._pad[1] = bytes[1];
614 }
615}
616
617const _: () = assert!(core::mem::size_of::<Fill>() == 40, "Fill size changed — ABI break");
619const _: () = assert!(core::mem::size_of::<Reject>() == 16, "Reject size changed — ABI break");
620
621#[derive(Debug, Clone, Copy, Default)]
630pub struct FillExt {
631 pub is_maker: u8,
633 pub fee_1e9: i64,
635 pub queue_ahead_1e8: i64,
637}
638
639pub mod time {
646 #[inline(always)]
648 pub fn start(now_ns: u64) -> u64 {
649 now_ns
650 }
651
652 #[inline(always)]
654 pub fn stop_ns(start_ns: u64, now_ns: u64) -> u64 {
655 now_ns.saturating_sub(start_ns)
656 }
657
658 #[inline(always)]
659 pub fn stop_us(start_ns: u64, now_ns: u64) -> u64 {
660 stop_ns(start_ns, now_ns) / 1_000
661 }
662
663 #[inline(always)]
664 pub fn stop_ms(start_ns: u64, now_ns: u64) -> u64 {
665 stop_ns(start_ns, now_ns) / 1_000_000
666 }
667
668 #[derive(Debug, Clone, Copy, Default)]
670 pub struct Timer {
671 start_ns: u64,
672 }
673
674 impl Timer {
675 #[inline(always)]
676 pub const fn new() -> Self {
677 Self { start_ns: 0 }
678 }
679
680 #[inline(always)]
681 pub fn start(&mut self, now_ns: u64) {
682 self.start_ns = now_ns;
683 }
684
685 #[inline(always)]
686 pub fn stop_ns(&self, now_ns: u64) -> u64 {
687 now_ns.saturating_sub(self.start_ns)
688 }
689
690 #[inline(always)]
691 pub fn stop_us(&self, now_ns: u64) -> u64 {
692 self.stop_ns(now_ns) / 1_000
693 }
694
695 #[inline(always)]
696 pub fn stop_ms(&self, now_ns: u64) -> u64 {
697 self.stop_ns(now_ns) / 1_000_000
698 }
699 }
700}
701
702pub mod RejectCode {
708 pub const UNKNOWN: u8 = 0;
710 pub const INSUFFICIENT_BALANCE: u8 = 1;
712 pub const INVALID_PARAMS: u8 = 2;
714 pub const RATE_LIMIT: u8 = 3;
716 pub const EXCHANGE_BUSY: u8 = 4;
718 pub const NETWORK: u8 = 5;
720 pub const AUTH: u8 = 6;
722
723 pub const RISK: u8 = 100;
726 pub const POSITION_LIMIT: u8 = 101;
728 pub const KILL_SWITCH: u8 = 102;
730 pub const PRICE_DEVIATION: u8 = 103;
732 pub const DAILY_LOSS_LIMIT: u8 = 104;
734 pub const BAD_VENUE: u8 = 105;
736 pub const STALE_VENUE: u8 = 106;
738
739 pub fn to_str(code: u8) -> &'static str {
741 match code {
742 UNKNOWN => "UNKNOWN",
743 INSUFFICIENT_BALANCE => "INSUFFICIENT_BALANCE",
744 INVALID_PARAMS => "INVALID_PARAMS",
745 RATE_LIMIT => "RATE_LIMIT",
746 EXCHANGE_BUSY => "EXCHANGE_BUSY",
747 NETWORK => "NETWORK",
748 AUTH => "AUTH",
749 RISK => "RISK_CHECK_FAILED",
750 POSITION_LIMIT => "POSITION_LIMIT",
751 KILL_SWITCH => "KILL_SWITCH",
752 PRICE_DEVIATION => "PRICE_DEVIATION",
753 DAILY_LOSS_LIMIT => "DAILY_LOSS_LIMIT",
754 BAD_VENUE => "BAD_VENUE",
755 STALE_VENUE => "STALE_VENUE",
756 _ => "UNKNOWN",
757 }
758 }
759}
760
761#[derive(Debug, Clone, Copy)]
762#[repr(C)]
763pub struct Reject {
764 pub order_id: u64,
765 pub code: u8,
766 pub _pad: [u8; 7],
767}
768
769impl Reject {
770 #[inline]
772 pub fn reason(&self) -> &'static str {
773 RejectCode::to_str(self.code)
774 }
775
776 #[inline(always)]
778 pub fn symbol_id(&self) -> u16 {
779 u16::from_le_bytes([self._pad[0], self._pad[1]])
780 }
781
782 #[inline(always)]
783 pub fn set_symbol_id(&mut self, id: u16) {
784 let bytes = id.to_le_bytes();
785 self._pad[0] = bytes[0];
786 self._pad[1] = bytes[1];
787 }
788}
789
790pub mod OrderType {
796 pub const LIMIT: u8 = 0;
798 pub const MARKET: u8 = 1;
800 pub const IOC: u8 = 2;
802 pub const FOK: u8 = 3;
804 pub const POST_ONLY: u8 = 4;
808}
809
810pub const ACTION_NEW: u8 = 0;
817pub const ACTION_CANCEL: u8 = 1;
819pub const ACTION_AMEND: u8 = 2;
821
822#[derive(Debug, Clone, Copy, Default)]
833#[repr(C)]
834pub struct Action {
835 pub order_id: u64,
836 pub px_1e9: u64,
837 pub qty_1e8: i64,
838 pub side: i8, pub is_cancel: u8,
840 pub order_type: u8, pub venue_id: u8, pub _pad: [u8; 4],
843}
844
845pub const MAX_ACTIONS: usize = 16;
847
848#[repr(C)]
850pub struct Actions {
851 actions: [Action; MAX_ACTIONS],
852 len: usize,
853}
854
855impl Actions {
856 #[inline(always)]
857 pub const fn new() -> Self {
858 Self {
859 actions: [Action {
860 order_id: 0,
861 px_1e9: 0,
862 qty_1e8: 0,
863 side: 0,
864 is_cancel: 0,
865 order_type: 0,
866 venue_id: 0,
867 _pad: [0; 4],
868 }; MAX_ACTIONS],
869 len: 0,
870 }
871 }
872
873 #[inline(always)]
874 pub fn clear(&mut self) {
875 self.len = 0;
876 }
877
878 #[inline(always)]
879 pub fn len(&self) -> usize {
880 self.len
881 }
882
883 #[inline(always)]
884 pub fn is_empty(&self) -> bool {
885 self.len == 0
886 }
887
888 #[inline(always)]
889 pub fn is_full(&self) -> bool {
890 self.len >= MAX_ACTIONS
891 }
892
893 #[inline(always)]
899 pub fn buy(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
900 self.order_typed(order_id, 1, qty_1e8, px_1e9, OrderType::LIMIT)
901 }
902
903 #[inline(always)]
905 pub fn sell(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
906 self.order_typed(order_id, -1, qty_1e8, px_1e9, OrderType::LIMIT)
907 }
908
909 #[inline(always)]
911 pub fn order(&mut self, order_id: u64, side: i8, qty_1e8: i64, px_1e9: u64) -> bool {
912 self.order_typed(order_id, side, qty_1e8, px_1e9, OrderType::LIMIT)
913 }
914
915 #[inline(always)]
921 pub fn market_buy(&mut self, order_id: u64, qty_1e8: i64) -> bool {
922 self.order_typed(order_id, 1, qty_1e8, 0, OrderType::MARKET)
923 }
924
925 #[inline(always)]
927 pub fn market_sell(&mut self, order_id: u64, qty_1e8: i64) -> bool {
928 self.order_typed(order_id, -1, qty_1e8, 0, OrderType::MARKET)
929 }
930
931 #[inline(always)]
937 pub fn ioc_buy(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
938 self.order_typed(order_id, 1, qty_1e8, px_1e9, OrderType::IOC)
939 }
940
941 #[inline(always)]
943 pub fn ioc_sell(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
944 self.order_typed(order_id, -1, qty_1e8, px_1e9, OrderType::IOC)
945 }
946
947 #[inline(always)]
953 pub fn fok_buy(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
954 self.order_typed(order_id, 1, qty_1e8, px_1e9, OrderType::FOK)
955 }
956
957 #[inline(always)]
959 pub fn fok_sell(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
960 self.order_typed(order_id, -1, qty_1e8, px_1e9, OrderType::FOK)
961 }
962
963 #[inline(always)]
969 pub fn post_only_buy(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
970 self.order_typed(order_id, 1, qty_1e8, px_1e9, OrderType::POST_ONLY)
971 }
972
973 #[inline(always)]
975 pub fn post_only_sell(&mut self, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
976 self.order_typed(order_id, -1, qty_1e8, px_1e9, OrderType::POST_ONLY)
977 }
978
979 #[inline(always)]
985 pub fn order_typed(
986 &mut self,
987 order_id: u64,
988 side: i8,
989 qty_1e8: i64,
990 px_1e9: u64,
991 order_type: u8,
992 ) -> bool {
993 if self.len >= MAX_ACTIONS {
994 return false;
995 }
996 self.actions[self.len] = Action {
997 order_id,
998 px_1e9,
999 qty_1e8,
1000 side,
1001 is_cancel: 0,
1002 order_type,
1003 venue_id: 0,
1004 _pad: [0; 4],
1005 };
1006 self.len += 1;
1007 true
1008 }
1009
1010 #[inline(always)]
1012 pub fn cancel(&mut self, order_id: u64) -> bool {
1013 if self.len >= MAX_ACTIONS {
1014 return false;
1015 }
1016 self.actions[self.len] = Action {
1017 order_id,
1018 px_1e9: 0,
1019 qty_1e8: 0,
1020 side: 0,
1021 is_cancel: 1,
1022 order_type: 0,
1023 venue_id: 0,
1024 _pad: [0; 4],
1025 };
1026 self.len += 1;
1027 true
1028 }
1029
1030 #[inline(always)]
1032 pub fn cancel_all(&mut self, state: &AlgoState) {
1033 for i in 0..state.order_ct as usize {
1034 let o = &state.orders[i];
1035 if o.is_live() && self.len < MAX_ACTIONS {
1036 self.cancel(o.order_id);
1037 }
1038 }
1039 }
1040
1041 #[inline(always)]
1053 pub fn amend(&mut self, order_id: u64, new_qty_1e8: i64, new_px_1e9: u64) -> bool {
1054 if self.len >= MAX_ACTIONS {
1055 return false;
1056 }
1057 self.actions[self.len] = Action {
1058 order_id,
1059 px_1e9: new_px_1e9,
1060 qty_1e8: new_qty_1e8,
1061 side: 0, is_cancel: ACTION_AMEND,
1063 order_type: 0,
1064 venue_id: 0,
1065 _pad: [0; 4],
1066 };
1067 self.len += 1;
1068 true
1069 }
1070
1071 #[inline(always)]
1073 pub fn amend_on(&mut self, venue_id: u8, order_id: u64, new_qty_1e8: i64, new_px_1e9: u64) -> bool {
1074 if self.len >= MAX_ACTIONS {
1075 return false;
1076 }
1077 self.actions[self.len] = Action {
1078 order_id,
1079 px_1e9: new_px_1e9,
1080 qty_1e8: new_qty_1e8,
1081 side: 0,
1082 is_cancel: ACTION_AMEND,
1083 order_type: 0,
1084 venue_id,
1085 _pad: [0; 4],
1086 };
1087 self.len += 1;
1088 true
1089 }
1090
1091 #[inline(always)]
1098 pub fn buy_on(&mut self, venue_id: u8, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
1099 self.order_on_venue(venue_id, order_id, 1, qty_1e8, px_1e9, OrderType::LIMIT)
1100 }
1101
1102 #[inline(always)]
1104 pub fn sell_on(&mut self, venue_id: u8, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
1105 self.order_on_venue(venue_id, order_id, -1, qty_1e8, px_1e9, OrderType::LIMIT)
1106 }
1107
1108 #[inline(always)]
1110 pub fn ioc_buy_on(&mut self, venue_id: u8, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
1111 self.order_on_venue(venue_id, order_id, 1, qty_1e8, px_1e9, OrderType::IOC)
1112 }
1113
1114 #[inline(always)]
1116 pub fn ioc_sell_on(&mut self, venue_id: u8, order_id: u64, qty_1e8: i64, px_1e9: u64) -> bool {
1117 self.order_on_venue(venue_id, order_id, -1, qty_1e8, px_1e9, OrderType::IOC)
1118 }
1119
1120 #[inline(always)]
1123 pub fn order_on_venue(
1124 &mut self,
1125 venue_id: u8,
1126 order_id: u64,
1127 side: i8,
1128 qty_1e8: i64,
1129 px_1e9: u64,
1130 order_type: u8,
1131 ) -> bool {
1132 if self.len >= MAX_ACTIONS {
1133 return false;
1134 }
1135 self.actions[self.len] = Action {
1136 order_id,
1137 px_1e9,
1138 qty_1e8,
1139 side,
1140 is_cancel: 0,
1141 order_type,
1142 venue_id,
1143 _pad: [0; 4],
1144 };
1145 self.len += 1;
1146 true
1147 }
1148
1149 #[inline(always)]
1152 pub fn clear_at(&mut self, idx: usize) {
1153 if idx < self.len {
1154 self.actions[idx] = Action {
1155 order_id: 0,
1156 px_1e9: 0,
1157 qty_1e8: 0,
1158 side: 0,
1159 is_cancel: 0,
1160 order_type: 0,
1161 venue_id: 0,
1162 _pad: [0; 4],
1163 };
1164 }
1165 }
1166
1167 #[inline(always)]
1168 pub fn get(&self, idx: usize) -> Option<&Action> {
1169 if idx < self.len {
1170 Some(&self.actions[idx])
1171 } else {
1172 None
1173 }
1174 }
1175
1176 #[inline(always)]
1177 pub fn iter(&self) -> impl Iterator<Item = &Action> {
1178 self.actions[..self.len].iter()
1179 }
1180}
1181
1182impl Default for Actions {
1183 fn default() -> Self {
1184 Self::new()
1185 }
1186}
1187
1188pub trait Algo: Send {
1195 fn on_book(&mut self, book: &L2Book, state: &AlgoState, actions: &mut Actions);
1200
1201 fn on_fill(&mut self, fill: &Fill, state: &AlgoState);
1203
1204 fn on_reject(&mut self, reject: &Reject);
1206
1207 fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions);
1209}
1210
1211pub trait AlgoV3: Send {
1222 fn on_book(
1224 &mut self,
1225 book: &L2Book,
1226 state: &AlgoState,
1227 features: &OnlineFeatures,
1228 actions: &mut Actions,
1229 );
1230
1231 fn on_fill(&mut self, fill: &Fill, state: &AlgoState);
1233
1234 fn on_reject(&mut self, reject: &Reject);
1236
1237 fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions);
1239
1240 fn on_heartbeat(
1242 &mut self,
1243 _state: &AlgoState,
1244 _features: &OnlineFeatures,
1245 _actions: &mut Actions,
1246 ) {
1247 }
1248}
1249
1250pub const MAX_VENUES: usize = 20; pub const VENUE_KRAKEN: u8 = 1;
1263pub const VENUE_COINBASE: u8 = 2;
1265pub const VENUE_BINANCE: u8 = 3;
1267pub const VENUE_BITGET: u8 = 4;
1269pub const VENUE_CRYPTOCOM: u8 = 5;
1271pub const VENUE_BITMART: u8 = 6;
1273pub const VENUE_DEX: u8 = 7;
1275pub const VENUE_OKX: u8 = 8;
1277pub const VENUE_BYBIT: u8 = 9;
1279pub const VENUE_UNKNOWN: u8 = 10;
1281pub const VENUE_DEX_ETH: u8 = 11;
1283pub const VENUE_DEX_ARB: u8 = 12;
1285pub const VENUE_DEX_BASE: u8 = 13;
1287pub const VENUE_DEX_OP: u8 = 14;
1289pub const VENUE_DEX_POLY: u8 = 15;
1291pub const VENUE_DEX_SOL: u8 = 16;
1293pub const VENUE_HYPERLIQUID: u8 = 17;
1295
1296#[inline(always)]
1298pub fn is_dex(venue_id: u8) -> bool {
1299 venue_id == VENUE_DEX
1300 || (venue_id >= VENUE_DEX_ETH && venue_id <= VENUE_DEX_POLY)
1301 || venue_id == VENUE_DEX_SOL
1302}
1303
1304#[inline(always)]
1306pub fn is_cex(venue_id: u8) -> bool {
1307 (venue_id >= VENUE_KRAKEN && venue_id <= VENUE_BYBIT && venue_id != VENUE_DEX)
1308 || venue_id == VENUE_HYPERLIQUID
1309}
1310
1311pub fn venue_name(venue_id: u8) -> &'static str {
1313 match venue_id {
1314 0 => "default",
1315 VENUE_KRAKEN => "kraken",
1316 VENUE_COINBASE => "coinbase",
1317 VENUE_BINANCE => "binance",
1318 VENUE_BITGET => "bitget",
1319 VENUE_CRYPTOCOM => "cryptocom",
1320 VENUE_BITMART => "bitmart",
1321 VENUE_DEX => "dex",
1322 VENUE_OKX => "okx",
1323 VENUE_BYBIT => "bybit",
1324 VENUE_UNKNOWN => "unknown",
1325 VENUE_DEX_ETH => "dex-eth",
1326 VENUE_DEX_ARB => "dex-arb",
1327 VENUE_DEX_BASE => "dex-base",
1328 VENUE_DEX_OP => "dex-op",
1329 VENUE_DEX_POLY => "dex-poly",
1330 VENUE_DEX_SOL => "dex-sol",
1331 VENUE_HYPERLIQUID => "hyperliquid",
1332 _ => "unknown",
1333 }
1334}
1335
1336#[derive(Clone, Copy, Debug)]
1348#[repr(C)]
1349pub struct NbboSnapshot {
1350 pub nbbo_bid_px_1e9: u64,
1352 pub nbbo_ask_px_1e9: u64,
1353 pub nbbo_bid_sz_1e8: u64,
1354 pub nbbo_ask_sz_1e8: u64,
1355 pub nbbo_bid_venue: u8, pub nbbo_ask_venue: u8, pub venue_ct: u8, pub _pad0: u8,
1360 pub sequence: u32, pub venue_ids: [u8; MAX_VENUES],
1363 pub venue_bid_px: [u64; MAX_VENUES],
1365 pub venue_ask_px: [u64; MAX_VENUES],
1366 pub venue_bid_sz: [u32; MAX_VENUES],
1367 pub venue_ask_sz: [u32; MAX_VENUES],
1368 pub venue_update_ms: [u16; MAX_VENUES], pub recv_ns: u64,
1372}
1373
1374impl Default for NbboSnapshot {
1375 fn default() -> Self {
1376 Self {
1377 nbbo_bid_px_1e9: 0,
1378 nbbo_ask_px_1e9: 0,
1379 nbbo_bid_sz_1e8: 0,
1380 nbbo_ask_sz_1e8: 0,
1381 nbbo_bid_venue: 0,
1382 nbbo_ask_venue: 0,
1383 venue_ct: 0,
1384 _pad0: 0,
1385 sequence: 0,
1386 venue_ids: [0; MAX_VENUES],
1387 venue_bid_px: [0; MAX_VENUES],
1388 venue_ask_px: [0; MAX_VENUES],
1389 venue_bid_sz: [0; MAX_VENUES],
1390 venue_ask_sz: [0; MAX_VENUES],
1391 venue_update_ms: [0; MAX_VENUES],
1392 recv_ns: 0,
1393 }
1394 }
1395}
1396
1397impl NbboSnapshot {
1398 #[inline(always)]
1401 pub fn nbbo_spread_bps(&self) -> u32 {
1402 if self.nbbo_bid_px_1e9 == 0 || self.nbbo_ask_px_1e9 == 0 {
1403 return u32::MAX;
1404 }
1405 let mid = (self.nbbo_bid_px_1e9 + self.nbbo_ask_px_1e9) / 2;
1406 if mid == 0 {
1407 return u32::MAX;
1408 }
1409 ((self.nbbo_ask_px_1e9.saturating_sub(self.nbbo_bid_px_1e9)) * 10_000 / mid) as u32
1410 }
1411
1412 #[inline(always)]
1415 pub fn nbbo_spread_bps_x1000(&self) -> i32 {
1416 if self.nbbo_bid_px_1e9 == 0 || self.nbbo_ask_px_1e9 == 0 {
1417 return i32::MAX;
1418 }
1419 let mid = (self.nbbo_bid_px_1e9 + self.nbbo_ask_px_1e9) / 2;
1420 if mid == 0 {
1421 return i32::MAX;
1422 }
1423 let spread = self.nbbo_ask_px_1e9 as i128 - self.nbbo_bid_px_1e9 as i128;
1424 ((spread * 10_000_000) / mid as i128) as i32
1425 }
1426
1427 #[inline(always)]
1429 pub fn is_crossed(&self) -> bool {
1430 self.nbbo_bid_px_1e9 > 0
1431 && self.nbbo_ask_px_1e9 > 0
1432 && self.nbbo_bid_px_1e9 > self.nbbo_ask_px_1e9
1433 && self.nbbo_bid_venue != self.nbbo_ask_venue
1434 }
1435
1436 #[inline(always)]
1438 pub fn venue_bid(&self, slot: usize) -> u64 {
1439 if slot < self.venue_ct as usize {
1440 self.venue_bid_px[slot]
1441 } else {
1442 0
1443 }
1444 }
1445
1446 #[inline(always)]
1448 pub fn venue_ask(&self, slot: usize) -> u64 {
1449 if slot < self.venue_ct as usize {
1450 self.venue_ask_px[slot]
1451 } else {
1452 0
1453 }
1454 }
1455
1456 #[inline(always)]
1458 pub fn is_venue_stale(&self, slot: usize, max_ms: u16) -> bool {
1459 if slot < self.venue_ct as usize {
1460 self.venue_update_ms[slot] > max_ms
1461 } else {
1462 true
1463 }
1464 }
1465
1466 #[inline]
1468 pub fn slot_for_venue(&self, venue_id: u8) -> Option<usize> {
1469 let ct = self.venue_ct as usize;
1470 for i in 0..ct {
1471 if self.venue_ids[i] == venue_id {
1472 return Some(i);
1473 }
1474 }
1475 None
1476 }
1477
1478 #[inline(always)]
1480 pub fn best_bid_venue_id(&self) -> u8 {
1481 self.venue_ids[self.nbbo_bid_venue as usize]
1482 }
1483
1484 #[inline(always)]
1486 pub fn best_ask_venue_id(&self) -> u8 {
1487 self.venue_ids[self.nbbo_ask_venue as usize]
1488 }
1489}
1490
1491pub const VENUE_BOOKS_WASM_OFFSET: u32 = 0x10000; #[derive(Clone)]
1518#[repr(C)]
1519pub struct VenueBooks {
1520 pub merged: L2Book,
1523 pub book_ct: u8,
1525 pub _pad: [u8; 7],
1526 pub venue_ids: [u8; MAX_VENUES],
1529 pub books: [L2Book; MAX_VENUES],
1533}
1534
1535impl Default for VenueBooks {
1536 fn default() -> Self {
1537 Self {
1538 merged: L2Book::default(),
1539 book_ct: 0,
1540 _pad: [0; 7],
1541 venue_ids: [0; MAX_VENUES],
1542 books: [L2Book::default(); MAX_VENUES],
1543 }
1544 }
1545}
1546
1547impl VenueBooks {
1548 #[inline]
1551 pub fn book_for_venue(&self, venue_id: u8) -> Option<&L2Book> {
1552 let ct = self.book_ct as usize;
1553 for i in 0..ct {
1554 if self.venue_ids[i] == venue_id {
1555 return Some(&self.books[i]);
1556 }
1557 }
1558 None
1559 }
1560
1561 #[inline(always)]
1564 pub fn book_at_slot(&self, slot: usize) -> &L2Book {
1565 if slot < self.book_ct as usize {
1566 &self.books[slot]
1567 } else {
1568 &self.books[0]
1570 }
1571 }
1572
1573 #[inline]
1575 pub fn has_depth_for(&self, venue_id: u8) -> bool {
1576 self.book_for_venue(venue_id)
1577 .map_or(false, |b| b.bid_ct > 0 || b.ask_ct > 0)
1578 }
1579
1580 #[inline(always)]
1582 pub fn venue_id_at(&self, slot: usize) -> u8 {
1583 if slot < self.book_ct as usize {
1584 self.venue_ids[slot]
1585 } else {
1586 0
1587 }
1588 }
1589
1590 #[inline]
1592 pub fn cex_count(&self) -> usize {
1593 let ct = self.book_ct as usize;
1594 (0..ct).filter(|&i| is_cex(self.venue_ids[i])).count()
1595 }
1596
1597 #[inline]
1599 pub fn dex_count(&self) -> usize {
1600 let ct = self.book_ct as usize;
1601 (0..ct).filter(|&i| is_dex(self.venue_ids[i])).count()
1602 }
1603}
1604
1605pub const MAX_POOLS: usize = 32;
1611
1612pub const POOL_BOOKS_WASM_OFFSET: u32 = 0x14000;
1615
1616#[derive(Clone, Copy)]
1621#[repr(C)]
1622pub struct PoolMeta {
1623 pub address: [u8; 32],
1625 pub pair_index: u16,
1627 pub fee_bps: u16,
1629 pub venue_id: u8,
1631 pub protocol_id: u8,
1635 pub _pad: [u8; 2],
1637}
1638
1639impl Default for PoolMeta {
1640 fn default() -> Self {
1641 Self {
1642 address: [0; 32],
1643 pair_index: 0,
1644 fee_bps: 0,
1645 venue_id: 0,
1646 protocol_id: 0,
1647 _pad: [0; 2],
1648 }
1649 }
1650}
1651
1652#[derive(Clone)]
1660#[repr(C)]
1661pub struct PoolBooks {
1662 pub pool_ct: u8,
1664 pub _pad: [u8; 7],
1665 pub metas: [PoolMeta; MAX_POOLS],
1667 pub books: [L2Book; MAX_POOLS],
1669}
1670
1671impl Default for PoolBooks {
1672 fn default() -> Self {
1673 Self {
1674 pool_ct: 0,
1675 _pad: [0; 7],
1676 metas: [PoolMeta::default(); MAX_POOLS],
1677 books: [L2Book::default(); MAX_POOLS],
1678 }
1679 }
1680}
1681
1682impl PoolBooks {
1683 #[inline]
1685 pub fn book_for_pool(&self, addr: &[u8; 32], pair_index: u16) -> Option<&L2Book> {
1686 let ct = self.pool_ct as usize;
1687 for i in 0..ct {
1688 if self.metas[i].address == *addr && self.metas[i].pair_index == pair_index {
1689 return Some(&self.books[i]);
1690 }
1691 }
1692 None
1693 }
1694
1695 #[inline(always)]
1697 pub fn book_at_slot(&self, slot: usize) -> &L2Book {
1698 if slot < self.pool_ct as usize {
1699 &self.books[slot]
1700 } else {
1701 &self.books[0]
1702 }
1703 }
1704
1705 #[inline(always)]
1707 pub fn meta_at_slot(&self, slot: usize) -> &PoolMeta {
1708 if slot < self.pool_ct as usize {
1709 &self.metas[slot]
1710 } else {
1711 &self.metas[0]
1712 }
1713 }
1714}
1715
1716pub const ONLINE_FEATURES_WASM_OFFSET: u32 = 0x1A000;
1723
1724#[derive(Clone, Copy, Debug)]
1728#[repr(C)]
1729pub struct OnlineFeatures {
1730 pub version: u16,
1732 pub flags: u16,
1734 pub _pad0: [u8; 4],
1735
1736 pub microprice_1e9: u64,
1739
1740 pub ofi_1level_1e8: i64,
1743 pub ofi_5level_1e8: i64,
1745 pub mlofi_10_1e8: i64,
1747 pub ofi_ewma_1e6: i64,
1749
1750 pub trade_sign_imbalance_1e6: i64,
1753 pub trade_arrival_rate_1e3: u32,
1755 pub vpin_1e4: u16,
1757 pub _pad1: u16,
1758
1759 pub spread_regime: u8,
1762 pub _pad2a: u8,
1763 pub spread_zscore_1e3: i16,
1765
1766 pub cancel_rate_1e4: u16,
1769 pub depth_imbalance_1e4: i16,
1771
1772 pub rv_1m_bps: u32,
1775 pub rv_5m_bps: u32,
1777 pub rv_1h_bps: u32,
1779 pub _pad3: u32,
1780
1781 pub pred_dir_up_1e4: u16,
1784 pub pred_dir_flat_1e4: u16,
1786 pub pred_dir_down_1e4: u16,
1788 pub pred_stress_normal_1e4: u16,
1790 pub pred_stress_widening_1e4: u16,
1792 pub pred_stress_crisis_1e4: u16,
1794 pub pred_toxic_1e4: u16,
1796 pub prediction_age_ms: u16,
1798
1799 pub fill_prob_bid_1e4: u16,
1802 pub fill_prob_ask_1e4: u16,
1804 pub queue_decay_rate_1e4: u16,
1806 pub _fill_pad: u16,
1807
1808 pub feature_ts_ns: u64,
1811
1812 pub _reserved: [u8; 136],
1814}
1815
1816impl Default for OnlineFeatures {
1817 fn default() -> Self {
1818 let mut f = unsafe { core::mem::zeroed::<Self>() };
1820 f.version = 1;
1821 f
1822 }
1823}
1824
1825impl OnlineFeatures {
1826 #[inline(always)]
1828 pub fn vpin_valid(&self) -> bool {
1829 self.flags & 1 != 0
1830 }
1831
1832 #[inline(always)]
1834 pub fn microprice_f64(&self) -> f64 {
1835 self.microprice_1e9 as f64 / 1_000_000_000.0
1836 }
1837
1838 #[inline(always)]
1840 pub fn ofi_1level_f64(&self) -> f64 {
1841 self.ofi_1level_1e8 as f64 / 100_000_000.0
1842 }
1843
1844 #[inline(always)]
1846 pub fn trade_sign_imbalance_f64(&self) -> f64 {
1847 self.trade_sign_imbalance_1e6 as f64 / 1_000_000.0
1848 }
1849}
1850
1851const _: () = assert!(
1853 core::mem::size_of::<OnlineFeatures>() == 256,
1854 "OnlineFeatures must be exactly 256 bytes"
1855);
1856const _: () = assert!(
1857 0x1A000 >= 0x14000 + core::mem::size_of::<PoolBooks>(),
1858 "ONLINE_FEATURES_WASM_OFFSET overlaps with PoolBooks"
1859);
1860const _: () = assert!(
1861 ONLINE_FEATURES_WASM_OFFSET as usize + core::mem::size_of::<OnlineFeatures>() < 0x1000000,
1862 "OnlineFeatures exceeds WASM 16MB memory limit"
1863);
1864
1865pub trait MultiVenueAlgo: Send {
1873 fn on_nbbo(
1883 &mut self,
1884 nbbo: &NbboSnapshot,
1885 books: &VenueBooks,
1886 state: &AlgoState,
1887 actions: &mut Actions,
1888 );
1889
1890 fn on_fill(&mut self, fill: &Fill, state: &AlgoState);
1892
1893 fn on_reject(&mut self, reject: &Reject);
1895
1896 fn on_shutdown(&mut self, state: &AlgoState, actions: &mut Actions);
1898}
1899
1900#[repr(C)]
1906pub struct WasmActions {
1907 pub count: u32,
1908 pub _pad: u32,
1909 pub actions: [Action; MAX_ACTIONS],
1910}
1911
1912impl WasmActions {
1913 pub const fn new() -> Self {
1914 Self {
1915 count: 0,
1916 _pad: 0,
1917 actions: [Action {
1918 order_id: 0,
1919 px_1e9: 0,
1920 qty_1e8: 0,
1921 side: 0,
1922 is_cancel: 0,
1923 order_type: 0,
1924 venue_id: 0,
1925 _pad: [0; 4],
1926 }; MAX_ACTIONS],
1927 }
1928 }
1929
1930 pub fn from_actions(&mut self, actions: &Actions) {
1931 self.count = actions.len() as u32;
1932 for i in 0..actions.len() {
1933 self.actions[i] = actions.actions[i];
1934 }
1935 }
1936}
1937
1938#[cfg(target_arch = "wasm32")]
1940#[macro_export]
1941macro_rules! export_algo {
1942 ($init:expr) => {
1943 extern crate alloc;
1944 static mut ALGO: Option<alloc::boxed::Box<dyn $crate::Algo>> = None;
1945 static mut ACTIONS: $crate::Actions = $crate::Actions::new();
1946 static mut WASM_OUT: $crate::WasmActions = $crate::WasmActions::new();
1947
1948 #[inline(always)]
1949 fn init() {
1950 unsafe {
1951 if ALGO.is_none() {
1952 ALGO = Some(alloc::boxed::Box::new($init));
1953 }
1954 }
1955 }
1956
1957 #[no_mangle]
1958 pub extern "C" fn algo_on_book(book_ptr: u32, state_ptr: u32) -> u32 {
1959 init();
1960 unsafe {
1961 let book = &*(book_ptr as *const $crate::L2Book);
1962 let state = &*(state_ptr as *const $crate::AlgoState);
1963 ACTIONS.clear();
1964 if let Some(algo) = ALGO.as_mut() {
1965 algo.on_book(book, state, &mut ACTIONS);
1966 }
1967 WASM_OUT.from_actions(&ACTIONS);
1968 &WASM_OUT as *const _ as u32
1969 }
1970 }
1971
1972 #[no_mangle]
1973 pub extern "C" fn algo_on_fill(fill_ptr: u32, state_ptr: u32) {
1974 init();
1975 unsafe {
1976 let fill = &*(fill_ptr as *const $crate::Fill);
1977 let state = &*(state_ptr as *const $crate::AlgoState);
1978 if let Some(algo) = ALGO.as_mut() {
1979 algo.on_fill(fill, state);
1980 }
1981 }
1982 }
1983
1984 #[no_mangle]
1985 pub extern "C" fn algo_on_reject(reject_ptr: u32) {
1986 init();
1987 unsafe {
1988 let reject = &*(reject_ptr as *const $crate::Reject);
1989 if let Some(algo) = ALGO.as_mut() {
1990 algo.on_reject(reject);
1991 }
1992 }
1993 }
1994
1995 #[no_mangle]
1996 pub extern "C" fn algo_on_shutdown(state_ptr: u32) -> u32 {
1997 init();
1998 unsafe {
1999 let state = &*(state_ptr as *const $crate::AlgoState);
2000 ACTIONS.clear();
2001 if let Some(algo) = ALGO.as_mut() {
2002 algo.on_shutdown(state, &mut ACTIONS);
2003 }
2004 WASM_OUT.from_actions(&ACTIONS);
2005 &WASM_OUT as *const _ as u32
2006 }
2007 }
2008
2009 #[no_mangle]
2010 pub extern "C" fn algo_alloc(size: u32) -> u32 {
2011 let layout = core::alloc::Layout::from_size_align(size as usize, 8).unwrap();
2012 unsafe { alloc::alloc::alloc(layout) as u32 }
2013 }
2014 };
2015}
2016
2017#[cfg(not(target_arch = "wasm32"))]
2019#[macro_export]
2020macro_rules! export_algo {
2021 ($init:expr) => {};
2022}
2023
2024#[cfg(target_arch = "wasm32")]
2032#[macro_export]
2033macro_rules! export_algo_v3 {
2034 ($init:expr) => {
2035 extern crate alloc;
2036 static mut ALGO: Option<alloc::boxed::Box<dyn $crate::AlgoV3>> = None;
2037 static mut ACTIONS: $crate::Actions = $crate::Actions::new();
2038 static mut WASM_OUT: $crate::WasmActions = $crate::WasmActions::new();
2039
2040 #[inline(always)]
2041 fn init() {
2042 unsafe {
2043 if ALGO.is_none() {
2044 ALGO = Some(alloc::boxed::Box::new($init));
2045 }
2046 }
2047 }
2048
2049 #[no_mangle]
2051 pub extern "C" fn algo_on_book_v3(book_ptr: u32, state_ptr: u32) -> u32 {
2052 init();
2053 unsafe {
2054 let book = &*(book_ptr as *const $crate::L2Book);
2055 let state = &*(state_ptr as *const $crate::AlgoState);
2056 let features =
2057 &*($crate::ONLINE_FEATURES_WASM_OFFSET as *const $crate::OnlineFeatures);
2058 ACTIONS.clear();
2059 if let Some(algo) = ALGO.as_mut() {
2060 algo.on_book(book, state, features, &mut ACTIONS);
2061 }
2062 WASM_OUT.from_actions(&ACTIONS);
2063 &WASM_OUT as *const _ as u32
2064 }
2065 }
2066
2067 #[no_mangle]
2069 pub extern "C" fn algo_on_book(_book_ptr: u32, _state_ptr: u32) -> u32 {
2070 0 }
2072
2073 #[no_mangle]
2075 pub extern "C" fn algo_on_heartbeat(state_ptr: u32) -> u32 {
2076 init();
2077 unsafe {
2078 let state = &*(state_ptr as *const $crate::AlgoState);
2079 let features =
2080 &*($crate::ONLINE_FEATURES_WASM_OFFSET as *const $crate::OnlineFeatures);
2081 ACTIONS.clear();
2082 if let Some(algo) = ALGO.as_mut() {
2083 algo.on_heartbeat(state, features, &mut ACTIONS);
2084 }
2085 WASM_OUT.from_actions(&ACTIONS);
2086 &WASM_OUT as *const _ as u32
2087 }
2088 }
2089
2090 #[no_mangle]
2091 pub extern "C" fn algo_on_fill(fill_ptr: u32, state_ptr: u32) {
2092 init();
2093 unsafe {
2094 let fill = &*(fill_ptr as *const $crate::Fill);
2095 let state = &*(state_ptr as *const $crate::AlgoState);
2096 if let Some(algo) = ALGO.as_mut() {
2097 algo.on_fill(fill, state);
2098 }
2099 }
2100 }
2101
2102 #[no_mangle]
2103 pub extern "C" fn algo_on_reject(reject_ptr: u32) {
2104 init();
2105 unsafe {
2106 let reject = &*(reject_ptr as *const $crate::Reject);
2107 if let Some(algo) = ALGO.as_mut() {
2108 algo.on_reject(reject);
2109 }
2110 }
2111 }
2112
2113 #[no_mangle]
2114 pub extern "C" fn algo_on_shutdown(state_ptr: u32) -> u32 {
2115 init();
2116 unsafe {
2117 let state = &*(state_ptr as *const $crate::AlgoState);
2118 ACTIONS.clear();
2119 if let Some(algo) = ALGO.as_mut() {
2120 algo.on_shutdown(state, &mut ACTIONS);
2121 }
2122 WASM_OUT.from_actions(&ACTIONS);
2123 &WASM_OUT as *const _ as u32
2124 }
2125 }
2126
2127 #[no_mangle]
2128 pub extern "C" fn algo_alloc(size: u32) -> u32 {
2129 let layout = core::alloc::Layout::from_size_align(size as usize, 8).unwrap();
2130 unsafe { alloc::alloc::alloc(layout) as u32 }
2131 }
2132 };
2133}
2134
2135#[cfg(not(target_arch = "wasm32"))]
2137#[macro_export]
2138macro_rules! export_algo_v3 {
2139 ($init:expr) => {};
2140}
2141
2142#[cfg(target_arch = "wasm32")]
2155#[macro_export]
2156macro_rules! export_multi_venue_algo {
2157 ($init:expr) => {
2158 extern crate alloc;
2159 static mut ALGO: Option<alloc::boxed::Box<dyn $crate::MultiVenueAlgo>> = None;
2160 static mut ACTIONS: $crate::Actions = $crate::Actions::new();
2161 static mut WASM_OUT: $crate::WasmActions = $crate::WasmActions::new();
2162
2163 #[inline(always)]
2164 fn init() {
2165 unsafe {
2166 if ALGO.is_none() {
2167 ALGO = Some(alloc::boxed::Box::new($init));
2168 }
2169 }
2170 }
2171
2172 #[no_mangle]
2175 pub extern "C" fn algo_on_nbbo(nbbo_ptr: u32, _book_ptr: u32, state_ptr: u32) -> u32 {
2176 init();
2177 unsafe {
2178 let nbbo = &*(nbbo_ptr as *const $crate::NbboSnapshot);
2179 let books = &*($crate::VENUE_BOOKS_WASM_OFFSET as *const $crate::VenueBooks);
2180 let state = &*(state_ptr as *const $crate::AlgoState);
2181 ACTIONS.clear();
2182 if let Some(algo) = ALGO.as_mut() {
2183 algo.on_nbbo(nbbo, books, state, &mut ACTIONS);
2184 }
2185 WASM_OUT.from_actions(&ACTIONS);
2186 &WASM_OUT as *const _ as u32
2187 }
2188 }
2189
2190 #[no_mangle]
2193 pub extern "C" fn algo_on_book(_book_ptr: u32, _state_ptr: u32) -> u32 {
2194 0 }
2196
2197 #[no_mangle]
2198 pub extern "C" fn algo_on_fill(fill_ptr: u32, state_ptr: u32) {
2199 init();
2200 unsafe {
2201 let fill = &*(fill_ptr as *const $crate::Fill);
2202 let state = &*(state_ptr as *const $crate::AlgoState);
2203 if let Some(algo) = ALGO.as_mut() {
2204 algo.on_fill(fill, state);
2205 }
2206 }
2207 }
2208
2209 #[no_mangle]
2210 pub extern "C" fn algo_on_reject(reject_ptr: u32) {
2211 init();
2212 unsafe {
2213 let reject = &*(reject_ptr as *const $crate::Reject);
2214 if let Some(algo) = ALGO.as_mut() {
2215 algo.on_reject(reject);
2216 }
2217 }
2218 }
2219
2220 #[no_mangle]
2221 pub extern "C" fn algo_on_shutdown(state_ptr: u32) -> u32 {
2222 init();
2223 unsafe {
2224 let state = &*(state_ptr as *const $crate::AlgoState);
2225 ACTIONS.clear();
2226 if let Some(algo) = ALGO.as_mut() {
2227 algo.on_shutdown(state, &mut ACTIONS);
2228 }
2229 WASM_OUT.from_actions(&ACTIONS);
2230 &WASM_OUT as *const _ as u32
2231 }
2232 }
2233
2234 #[no_mangle]
2235 pub extern "C" fn algo_alloc(size: u32) -> u32 {
2236 let layout = core::alloc::Layout::from_size_align(size as usize, 8).unwrap();
2237 unsafe { alloc::alloc::alloc(layout) as u32 }
2238 }
2239 };
2240}
2241
2242#[cfg(not(target_arch = "wasm32"))]
2244#[macro_export]
2245macro_rules! export_multi_venue_algo {
2246 ($init:expr) => {};
2247}
2248
2249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2255#[repr(u8)]
2256pub enum LogLevel {
2257 Trace = 0,
2258 Debug = 1,
2259 Info = 2,
2260 Warn = 3,
2261 Error = 4,
2262}
2263
2264#[cfg(target_arch = "wasm32")]
2266extern "C" {
2267 fn host_log_impl(level: u8, ptr: *const u8, len: u32);
2268}
2269
2270#[cfg(target_arch = "wasm32")]
2271#[inline(always)]
2272fn host_log(level: u8, ptr: *const u8, len: u32) {
2273 unsafe {
2274 host_log_impl(level, ptr, len);
2275 }
2276}
2277
2278#[cfg(not(target_arch = "wasm32"))]
2280#[inline(always)]
2281fn host_log(_level: u8, _ptr: *const u8, _len: u32) {}
2282
2283pub mod log {
2286 use super::LogLevel;
2287 use core::fmt::Write;
2288
2289 struct LogBuf {
2291 buf: [u8; 255],
2292 pos: usize,
2293 }
2294
2295 impl LogBuf {
2296 #[inline(always)]
2297 const fn new() -> Self {
2298 Self {
2299 buf: [0u8; 255],
2300 pos: 0,
2301 }
2302 }
2303
2304 #[inline(always)]
2305 fn as_slice(&self) -> &[u8] {
2306 &self.buf[..self.pos]
2307 }
2308 }
2309
2310 impl Write for LogBuf {
2311 fn write_str(&mut self, s: &str) -> core::fmt::Result {
2312 let bytes = s.as_bytes();
2313 let space = self.buf.len() - self.pos;
2314 let n = bytes.len().min(space);
2315 self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
2316 self.pos += n;
2317 Ok(())
2318 }
2319 }
2320
2321 #[inline(always)]
2322 fn send(level: LogLevel, msg: &[u8]) {
2323 super::host_log(level as u8, msg.as_ptr(), msg.len() as u32);
2324 }
2325
2326 #[inline(always)]
2328 pub fn trace(msg: &str) {
2329 send(LogLevel::Trace, msg.as_bytes());
2330 }
2331
2332 #[inline(always)]
2334 pub fn debug(msg: &str) {
2335 send(LogLevel::Debug, msg.as_bytes());
2336 }
2337
2338 #[inline(always)]
2340 pub fn info(msg: &str) {
2341 send(LogLevel::Info, msg.as_bytes());
2342 }
2343
2344 #[inline(always)]
2346 pub fn warn(msg: &str) {
2347 send(LogLevel::Warn, msg.as_bytes());
2348 }
2349
2350 #[inline(always)]
2352 pub fn error(msg: &str) {
2353 send(LogLevel::Error, msg.as_bytes());
2354 }
2355
2356 #[inline]
2358 pub fn log_fmt(level: LogLevel, args: core::fmt::Arguments<'_>) {
2359 let mut buf = LogBuf::new();
2360 let _ = buf.write_fmt(args);
2361 send(level, buf.as_slice());
2362 }
2363
2364 #[inline]
2366 pub fn info_fmt(args: core::fmt::Arguments<'_>) {
2367 log_fmt(LogLevel::Info, args);
2368 }
2369
2370 #[inline]
2372 pub fn warn_fmt(args: core::fmt::Arguments<'_>) {
2373 log_fmt(LogLevel::Warn, args);
2374 }
2375
2376 #[inline]
2378 pub fn error_fmt(args: core::fmt::Arguments<'_>) {
2379 log_fmt(LogLevel::Error, args);
2380 }
2381
2382 #[inline]
2384 pub fn debug_fmt(args: core::fmt::Arguments<'_>) {
2385 log_fmt(LogLevel::Debug, args);
2386 }
2387}
2388
2389#[macro_export]
2391macro_rules! log_info {
2392 ($($arg:tt)*) => {
2393 $crate::log::info_fmt(format_args!($($arg)*))
2394 };
2395}
2396
2397#[macro_export]
2399macro_rules! log_warn {
2400 ($($arg:tt)*) => {
2401 $crate::log::warn_fmt(format_args!($($arg)*))
2402 };
2403}
2404
2405#[macro_export]
2407macro_rules! log_error {
2408 ($($arg:tt)*) => {
2409 $crate::log::error_fmt(format_args!($($arg)*))
2410 };
2411}
2412
2413#[macro_export]
2415macro_rules! log_debug {
2416 ($($arg:tt)*) => {
2417 $crate::log::debug_fmt(format_args!($($arg)*))
2418 };
2419}
2420
2421#[cfg(test)]
2426mod tests {
2427 use super::*;
2428
2429 fn make_book(bid_px: u64, bid_sz: u64, ask_px: u64, ask_sz: u64) -> L2Book {
2431 let mut book = L2Book::default();
2432 if bid_px > 0 {
2433 book.bids[0] = Level {
2434 px_1e9: bid_px,
2435 sz_1e8: bid_sz,
2436 };
2437 book.bid_ct = 1;
2438 }
2439 if ask_px > 0 {
2440 book.asks[0] = Level {
2441 px_1e9: ask_px,
2442 sz_1e8: ask_sz,
2443 };
2444 book.ask_ct = 1;
2445 }
2446 book
2447 }
2448
2449 #[test]
2452 fn level_empty_is_zero() {
2453 let l = Level::EMPTY;
2454 assert_eq!(l.px_1e9, 0);
2455 assert_eq!(l.sz_1e8, 0);
2456 }
2457
2458 #[test]
2459 fn level_is_valid() {
2460 assert!(!Level::EMPTY.is_valid());
2461 assert!(Level {
2462 px_1e9: 1,
2463 sz_1e8: 0
2464 }
2465 .is_valid());
2466 }
2467
2468 #[test]
2471 fn book_best_bid_ask_empty() {
2472 let book = L2Book::default();
2473 assert!(book.best_bid().is_none());
2474 assert!(book.best_ask().is_none());
2475 }
2476
2477 #[test]
2478 fn book_best_bid_ask_populated() {
2479 let book = make_book(99_000_000_000, 1_000_000, 101_000_000_000, 2_000_000);
2480 let bid = book.best_bid().unwrap();
2481 assert_eq!(bid.px_1e9, 99_000_000_000);
2482 let ask = book.best_ask().unwrap();
2483 assert_eq!(ask.px_1e9, 101_000_000_000);
2484 }
2485
2486 #[test]
2487 fn book_mid_px_empty() {
2488 assert_eq!(L2Book::default().mid_px_1e9(), 0);
2489 }
2490
2491 #[test]
2492 fn book_mid_px_correct() {
2493 let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
2494 assert_eq!(book.mid_px_1e9(), 100_000_000_000);
2495 }
2496
2497 #[test]
2498 fn book_spread_empty() {
2499 assert_eq!(L2Book::default().spread_1e9(), u64::MAX);
2500 }
2501
2502 #[test]
2503 fn book_spread_correct() {
2504 let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
2505 assert_eq!(book.spread_1e9(), 2_000_000_000);
2506 }
2507
2508 #[test]
2509 fn book_spread_bps_empty() {
2510 assert_eq!(L2Book::default().spread_bps(), u32::MAX);
2511 }
2512
2513 #[test]
2514 fn book_spread_bps_correct() {
2515 let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
2517 assert_eq!(book.spread_bps(), 200);
2518 }
2519
2520 #[test]
2521 fn book_spread_bps_x1000_sub_bps() {
2522 let book = make_book(67_270_000_000_000, 1, 67_270_100_000_000, 1);
2526 assert_eq!(book.spread_bps(), 0); let mbps = book.spread_bps_x1000();
2528 assert!(mbps > 0, "milli-bps should be positive: {}", mbps);
2529 assert_eq!(mbps, 14); }
2531
2532 #[test]
2533 fn book_spread_bps_x1000_crossed() {
2534 let book = make_book(67_270_000_000_000, 1, 67_261_990_000_000, 1);
2536 assert!(book.is_crossed());
2537 let mbps = book.spread_bps_x1000();
2538 assert!(mbps < 0, "crossed book should have negative milli-bps: {}", mbps);
2539 }
2540
2541 #[test]
2542 fn book_spread_bps_x1000_normal() {
2543 let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
2545 assert!(!book.is_crossed());
2546 assert_eq!(book.spread_bps_x1000(), 200_000);
2547 }
2548
2549 #[test]
2550 fn book_is_crossed() {
2551 let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
2553 assert!(!book.is_crossed());
2554
2555 let book = make_book(101_000_000_000, 1, 99_000_000_000, 1);
2557 assert!(book.is_crossed());
2558
2559 assert!(!L2Book::default().is_crossed());
2561 }
2562
2563 #[test]
2564 fn book_spread_signed_1e9() {
2565 let book = make_book(99_000_000_000, 1, 101_000_000_000, 1);
2567 assert_eq!(book.spread_signed_1e9(), 2_000_000_000);
2568
2569 let book = make_book(101_000_000_000, 1, 99_000_000_000, 1);
2571 assert_eq!(book.spread_signed_1e9(), -2_000_000_000);
2572 }
2573
2574 #[test]
2575 fn book_depth_and_imbalance() {
2576 let mut book = make_book(100_000_000_000, 500, 101_000_000_000, 300);
2577 book.bids[1] = Level {
2579 px_1e9: 99_000_000_000,
2580 sz_1e8: 200,
2581 };
2582 book.bid_ct = 2;
2583
2584 assert_eq!(book.bid_depth_1e8(1), 500);
2585 assert_eq!(book.bid_depth_1e8(5), 700); assert_eq!(book.ask_depth_1e8(1), 300);
2587
2588 let imb = book.imbalance_bps(5);
2590 assert_eq!(imb, 4000);
2591 }
2592
2593 #[test]
2594 fn book_imbalance_empty() {
2595 assert_eq!(L2Book::default().imbalance_bps(5), 0);
2596 }
2597
2598 #[test]
2601 fn open_order_is_live() {
2602 let mut o = OpenOrder::EMPTY;
2603 o.status = Status::ACKED;
2604 assert!(o.is_live());
2605 o.status = Status::PARTIAL;
2606 assert!(o.is_live());
2607 o.status = Status::PENDING;
2608 assert!(!o.is_live());
2609 o.status = Status::DEAD;
2610 assert!(!o.is_live());
2611 }
2612
2613 #[test]
2614 fn open_order_is_pending() {
2615 let mut o = OpenOrder::EMPTY;
2616 o.status = Status::PENDING;
2617 assert!(o.is_pending());
2618 o.status = Status::ACKED;
2619 assert!(!o.is_pending());
2620 }
2621
2622 #[test]
2623 fn open_order_remaining() {
2624 let o = OpenOrder {
2625 qty_1e8: 100,
2626 filled_1e8: 30,
2627 ..OpenOrder::EMPTY
2628 };
2629 assert_eq!(o.remaining_1e8(), 70);
2630
2631 let o2 = OpenOrder {
2633 qty_1e8: -100,
2634 filled_1e8: -40,
2635 ..OpenOrder::EMPTY
2636 };
2637 assert_eq!(o2.remaining_1e8(), 60);
2638 }
2639
2640 #[test]
2643 fn symbol_round_px() {
2644 let meta = SymbolMeta {
2645 tick_size_1e9: 10_000_000,
2646 ..SymbolMeta::EMPTY
2647 };
2648 assert_eq!(meta.round_px(95_000_000), 90_000_000);
2650 assert_eq!(SymbolMeta::EMPTY.round_px(95_000_000), 95_000_000);
2652 }
2653
2654 #[test]
2655 fn symbol_round_qty() {
2656 let meta = SymbolMeta {
2657 lot_size_1e8: 1_000_000,
2658 ..SymbolMeta::EMPTY
2659 };
2660 assert_eq!(meta.round_qty(1_500_000), 1_000_000);
2661 assert_eq!(SymbolMeta::EMPTY.round_qty(1_500_000), 1_500_000);
2662 }
2663
2664 #[test]
2665 fn symbol_check_notional() {
2666 assert!(SymbolMeta::EMPTY.check_notional(1, 1));
2668 let meta = SymbolMeta {
2669 min_notional_1e9: 10_000_000_000,
2670 ..SymbolMeta::EMPTY
2671 };
2672 assert!(meta.check_notional(100_000_000, 100_000_000_000));
2674 assert!(!meta.check_notional(1, 1_000_000_000));
2676 }
2677
2678 #[test]
2679 fn symbol_check_min_qty() {
2680 assert!(SymbolMeta::EMPTY.check_min_qty(1));
2681 let meta = SymbolMeta {
2682 min_qty_1e8: 100,
2683 ..SymbolMeta::EMPTY
2684 };
2685 assert!(meta.check_min_qty(100));
2686 assert!(meta.check_min_qty(-100)); assert!(!meta.check_min_qty(99));
2688 }
2689
2690 #[test]
2693 fn risk_check_position() {
2694 assert!(RiskSnapshot::EMPTY.check_position(999, 999));
2696
2697 let risk = RiskSnapshot {
2698 max_position_1e8: 100,
2699 ..RiskSnapshot::default()
2700 };
2701 assert!(risk.check_position(50, 40)); assert!(risk.check_position(50, 50)); assert!(!risk.check_position(50, 51)); assert!(risk.check_position(-50, -50)); assert!(!risk.check_position(-50, -51));
2707 }
2708
2709 #[test]
2712 fn algo_state_position_flags() {
2713 let mut s = AlgoState::default();
2714 assert!(s.is_flat());
2715 assert!(!s.is_long());
2716 assert!(!s.is_short());
2717
2718 s.position_1e8 = 100;
2719 assert!(s.is_long());
2720 assert!(!s.is_flat());
2721
2722 s.position_1e8 = -100;
2723 assert!(s.is_short());
2724 }
2725
2726 #[test]
2727 fn algo_state_live_order_count() {
2728 let mut s = AlgoState::default();
2729 s.order_ct = 3;
2730 s.orders[0] = OpenOrder {
2731 status: Status::ACKED,
2732 ..OpenOrder::EMPTY
2733 };
2734 s.orders[1] = OpenOrder {
2735 status: Status::PENDING,
2736 ..OpenOrder::EMPTY
2737 };
2738 s.orders[2] = OpenOrder {
2739 status: Status::PARTIAL,
2740 ..OpenOrder::EMPTY
2741 };
2742 assert_eq!(s.live_order_count(), 2); }
2744
2745 #[test]
2746 fn algo_state_find_order() {
2747 let mut s = AlgoState::default();
2748 s.order_ct = 1;
2749 s.orders[0] = OpenOrder {
2750 order_id: 42,
2751 ..OpenOrder::EMPTY
2752 };
2753 assert!(s.find_order(42).is_some());
2754 assert!(s.find_order(99).is_none());
2755 }
2756
2757 #[test]
2758 fn algo_state_open_buy_sell_qty() {
2759 let mut s = AlgoState::default();
2760 s.order_ct = 2;
2761 s.orders[0] = OpenOrder {
2762 order_id: 1,
2763 side: 1,
2764 qty_1e8: 100,
2765 filled_1e8: 20,
2766 status: Status::ACKED,
2767 ..OpenOrder::EMPTY
2768 };
2769 s.orders[1] = OpenOrder {
2770 order_id: 2,
2771 side: -1,
2772 qty_1e8: -200,
2773 filled_1e8: -50,
2774 status: Status::PARTIAL,
2775 ..OpenOrder::EMPTY
2776 };
2777 assert_eq!(s.open_buy_qty_1e8(), 80); assert_eq!(s.open_sell_qty_1e8(), 150); }
2780
2781 #[test]
2782 fn algo_state_pnl() {
2783 let mut s = AlgoState::default();
2784 s.realized_pnl_1e9 = 5_000_000_000;
2785 s.unrealized_pnl_1e9 = -2_000_000_000;
2786 s.session_pnl_1e9 = 1_500_000_000;
2787
2788 assert_eq!(s.total_pnl_1e9(), 3_000_000_000);
2789
2790 let snap = s.get_pnl();
2791 assert_eq!(snap.realized_1e9, 5_000_000_000);
2792 assert_eq!(snap.unrealized_1e9, -2_000_000_000);
2793 assert_eq!(snap.total_1e9, 3_000_000_000);
2794
2795 assert!((s.realized_pnl_usd() - 5.0).abs() < 1e-9);
2796 assert!((s.unrealized_pnl_usd() - (-2.0)).abs() < 1e-9);
2797 assert!((s.total_pnl_usd() - 3.0).abs() < 1e-9);
2798 assert!((s.session_pnl_usd() - 1.5).abs() < 1e-9);
2799 }
2800
2801 #[test]
2804 fn fill_since() {
2805 let fill = Fill {
2806 order_id: 1,
2807 px_1e9: 0,
2808 qty_1e8: 0,
2809 recv_ns: 10_000_000,
2810 side: 1,
2811 _pad: [0; 7],
2812 };
2813 assert_eq!(fill.since_ns(5_000_000), 5_000_000);
2814 assert_eq!(fill.since_us(5_000_000), 5_000);
2815 assert_eq!(fill.since_ms(5_000_000), 5);
2816 assert_eq!(fill.since_ns(99_000_000), 0);
2818 }
2819
2820 #[test]
2823 fn time_helpers() {
2824 let t = time::start(1000);
2825 assert_eq!(t, 1000);
2826 assert_eq!(time::stop_ns(1000, 2500), 1500);
2827 assert_eq!(time::stop_us(1000, 2_001_000), 2_000);
2828 assert_eq!(time::stop_ms(1000, 5_000_001_000), 5_000);
2829 assert_eq!(time::stop_ns(5000, 1000), 0);
2831 }
2832
2833 #[test]
2834 fn timer_struct() {
2835 let mut t = time::Timer::new();
2836 t.start(1_000_000);
2837 assert_eq!(t.stop_ns(3_000_000), 2_000_000);
2838 assert_eq!(t.stop_us(3_000_000), 2_000);
2839 assert_eq!(t.stop_ms(3_000_000), 2);
2840 assert_eq!(t.stop_ns(500_000), 0);
2842 }
2843
2844 #[test]
2847 fn reject_code_to_str() {
2848 assert_eq!(RejectCode::to_str(RejectCode::UNKNOWN), "UNKNOWN");
2849 assert_eq!(
2850 RejectCode::to_str(RejectCode::INSUFFICIENT_BALANCE),
2851 "INSUFFICIENT_BALANCE"
2852 );
2853 assert_eq!(RejectCode::to_str(RejectCode::RATE_LIMIT), "RATE_LIMIT");
2854 assert_eq!(RejectCode::to_str(RejectCode::RISK), "RISK_CHECK_FAILED");
2855 assert_eq!(
2856 RejectCode::to_str(RejectCode::DAILY_LOSS_LIMIT),
2857 "DAILY_LOSS_LIMIT"
2858 );
2859 assert_eq!(RejectCode::to_str(255), "UNKNOWN");
2860 }
2861
2862 #[test]
2863 fn reject_reason_delegates() {
2864 let r = Reject {
2865 order_id: 1,
2866 code: RejectCode::AUTH,
2867 _pad: [0; 7],
2868 };
2869 assert_eq!(r.reason(), "AUTH");
2870 }
2871
2872 #[test]
2875 fn order_type_values() {
2876 assert_eq!(OrderType::LIMIT, 0);
2877 assert_eq!(OrderType::MARKET, 1);
2878 assert_eq!(OrderType::IOC, 2);
2879 assert_eq!(OrderType::FOK, 3);
2880 assert_eq!(OrderType::POST_ONLY, 4);
2881 }
2882
2883 #[test]
2886 fn log_level_discriminants() {
2887 assert_eq!(LogLevel::Trace as u8, 0);
2888 assert_eq!(LogLevel::Debug as u8, 1);
2889 assert_eq!(LogLevel::Info as u8, 2);
2890 assert_eq!(LogLevel::Warn as u8, 3);
2891 assert_eq!(LogLevel::Error as u8, 4);
2892 }
2893
2894 #[test]
2897 fn actions_new_is_empty() {
2898 let a = Actions::new();
2899 assert_eq!(a.len(), 0);
2900 assert!(a.is_empty());
2901 assert!(!a.is_full());
2902 }
2903
2904 #[test]
2905 fn actions_buy_sell() {
2906 let mut a = Actions::new();
2907 assert!(a.buy(1, 100, 50_000));
2908 assert!(a.sell(2, 200, 60_000));
2909 assert_eq!(a.len(), 2);
2910
2911 let buy = a.get(0).unwrap();
2912 assert_eq!(buy.side, 1);
2913 assert_eq!(buy.qty_1e8, 100);
2914 assert_eq!(buy.px_1e9, 50_000);
2915 assert_eq!(buy.order_type, OrderType::LIMIT);
2916 assert_eq!(buy.is_cancel, 0);
2917
2918 let sell = a.get(1).unwrap();
2919 assert_eq!(sell.side, -1);
2920 assert_eq!(sell.qty_1e8, 200);
2921 }
2922
2923 #[test]
2924 fn actions_cancel() {
2925 let mut a = Actions::new();
2926 assert!(a.cancel(42));
2927 let c = a.get(0).unwrap();
2928 assert_eq!(c.is_cancel, 1);
2929 assert_eq!(c.order_id, 42);
2930 }
2931
2932 #[test]
2933 fn actions_full_rejects() {
2934 let mut a = Actions::new();
2935 for i in 0..MAX_ACTIONS {
2936 assert!(a.buy(i as u64, 1, 1));
2937 }
2938 assert!(a.is_full());
2939 assert!(!a.buy(999, 1, 1));
2940 }
2941
2942 #[test]
2943 fn actions_clear() {
2944 let mut a = Actions::new();
2945 a.buy(1, 1, 1);
2946 a.buy(2, 1, 1);
2947 a.clear();
2948 assert_eq!(a.len(), 0);
2949 assert!(a.is_empty());
2950 }
2951
2952 #[test]
2953 fn actions_market_orders() {
2954 let mut a = Actions::new();
2955 a.market_buy(1, 100);
2956 a.market_sell(2, 200);
2957 let mb = a.get(0).unwrap();
2958 assert_eq!(mb.order_type, OrderType::MARKET);
2959 assert_eq!(mb.px_1e9, 0);
2960 assert_eq!(mb.side, 1);
2961 let ms = a.get(1).unwrap();
2962 assert_eq!(ms.order_type, OrderType::MARKET);
2963 assert_eq!(ms.side, -1);
2964 }
2965
2966 #[test]
2967 fn actions_ioc_fok_post_only() {
2968 let mut a = Actions::new();
2969 a.ioc_buy(1, 10, 100);
2970 a.fok_buy(2, 20, 200);
2971 a.post_only_buy(3, 30, 300);
2972 assert_eq!(a.get(0).unwrap().order_type, OrderType::IOC);
2973 assert_eq!(a.get(1).unwrap().order_type, OrderType::FOK);
2974 assert_eq!(a.get(2).unwrap().order_type, OrderType::POST_ONLY);
2975 }
2976
2977 #[test]
2978 fn actions_cancel_all() {
2979 let mut state = AlgoState::default();
2980 state.order_ct = 3;
2981 state.orders[0] = OpenOrder {
2982 order_id: 10,
2983 status: Status::ACKED,
2984 ..OpenOrder::EMPTY
2985 };
2986 state.orders[1] = OpenOrder {
2987 order_id: 11,
2988 status: Status::DEAD,
2989 ..OpenOrder::EMPTY
2990 };
2991 state.orders[2] = OpenOrder {
2992 order_id: 12,
2993 status: Status::PARTIAL,
2994 ..OpenOrder::EMPTY
2995 };
2996
2997 let mut a = Actions::new();
2998 a.cancel_all(&state);
2999 assert_eq!(a.len(), 2);
3001 assert_eq!(a.get(0).unwrap().order_id, 10);
3002 assert_eq!(a.get(1).unwrap().order_id, 12);
3003 }
3004
3005 #[test]
3006 fn actions_clear_at() {
3007 let mut a = Actions::new();
3008 a.buy(1, 100, 50_000);
3009 a.buy(2, 200, 60_000);
3010 a.clear_at(0);
3011 let zeroed = a.get(0).unwrap();
3012 assert_eq!(zeroed.order_id, 0);
3013 assert_eq!(zeroed.qty_1e8, 0);
3014 assert_eq!(a.len(), 2);
3016 }
3017
3018 #[test]
3019 fn actions_get_out_of_bounds() {
3020 let a = Actions::new();
3021 assert!(a.get(0).is_none());
3022 assert!(a.get(100).is_none());
3023 }
3024
3025 #[test]
3026 fn actions_iter() {
3027 let mut a = Actions::new();
3028 a.buy(1, 1, 1);
3029 a.sell(2, 1, 1);
3030 a.cancel(3);
3031 assert_eq!(a.iter().count(), 3);
3032 }
3033
3034 #[test]
3037 fn abi_action_size_is_32() {
3038 assert_eq!(core::mem::size_of::<Action>(), 32);
3039 }
3040
3041 #[test]
3042 fn abi_action_field_offsets() {
3043 use core::mem;
3044 assert_eq!(mem::offset_of!(Action, order_id), 0);
3047 assert_eq!(mem::offset_of!(Action, px_1e9), 8);
3048 assert_eq!(mem::offset_of!(Action, qty_1e8), 16);
3049 assert_eq!(mem::offset_of!(Action, side), 24);
3050 assert_eq!(mem::offset_of!(Action, is_cancel), 25);
3051 assert_eq!(mem::offset_of!(Action, order_type), 26);
3052 assert_eq!(mem::offset_of!(Action, venue_id), 27);
3053 assert_eq!(mem::offset_of!(Action, _pad), 28);
3054 }
3055
3056 #[test]
3057 fn abi_nbbo_snapshot_fits_512() {
3058 assert!(core::mem::size_of::<NbboSnapshot>() <= 640);
3059 }
3060
3061 #[test]
3062 fn abi_l2book_size_unchanged() {
3063 let size = core::mem::size_of::<L2Book>();
3065 assert_eq!(size, 656, "L2Book size changed — ABI break!");
3066 }
3067
3068 #[test]
3069 fn abi_fill_size_unchanged() {
3070 assert_eq!(core::mem::size_of::<Fill>(), 40);
3071 }
3072
3073 #[test]
3074 fn abi_reject_size_unchanged() {
3075 assert_eq!(core::mem::size_of::<Reject>(), 16);
3076 }
3077
3078 #[test]
3079 fn abi_action_default_venue_is_zero() {
3080 let a = Action::default();
3082 assert_eq!(a.venue_id, 0);
3083 }
3084
3085 #[test]
3088 fn venue_index_convention_slot_to_id() {
3089 let mut snap = NbboSnapshot::default();
3090 snap.venue_ct = 3;
3091 snap.venue_ids = [10, 20, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
3092 snap.venue_bid_px[0] = 100; snap.venue_bid_px[1] = 200; snap.venue_bid_px[2] = 150; snap.nbbo_bid_venue = 1; assert_eq!(snap.venue_ids[snap.nbbo_bid_venue as usize], 20);
3099 assert_eq!(snap.venue_bid_px[snap.nbbo_bid_venue as usize], 200);
3100
3101 assert_eq!(snap.slot_for_venue(20), Some(1));
3103 assert_eq!(snap.slot_for_venue(10), Some(0));
3104 assert_eq!(snap.slot_for_venue(30), Some(2));
3105 assert_eq!(snap.slot_for_venue(99), None);
3106 }
3107
3108 #[test]
3109 fn venue_index_convention_best_venue_id() {
3110 let mut snap = NbboSnapshot::default();
3111 snap.venue_ct = 2;
3112 snap.venue_ids = [5, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
3113 snap.nbbo_bid_venue = 0; snap.nbbo_ask_venue = 1; assert_eq!(snap.best_bid_venue_id(), 5);
3117 assert_eq!(snap.best_ask_venue_id(), 15);
3118 }
3119
3120 #[test]
3121 fn action_venue_id_is_value_not_slot() {
3122 let mut a = Actions::new();
3124 a.buy_on(20, 1, 100, 50_000);
3125 let action = a.get(0).unwrap();
3126 assert_eq!(action.venue_id, 20); assert_eq!(action.side, 1);
3128 assert_eq!(action.order_type, OrderType::LIMIT);
3129 }
3130
3131 #[test]
3132 fn nbbo_spread_bps_empty() {
3133 assert_eq!(NbboSnapshot::default().nbbo_spread_bps(), u32::MAX);
3134 }
3135
3136 #[test]
3137 fn nbbo_spread_bps_correct() {
3138 let mut snap = NbboSnapshot::default();
3139 snap.nbbo_bid_px_1e9 = 99_000_000_000;
3140 snap.nbbo_ask_px_1e9 = 101_000_000_000;
3141 assert_eq!(snap.nbbo_spread_bps(), 200);
3143 }
3144
3145 #[test]
3146 fn nbbo_spread_bps_x1000_sub_bps() {
3147 let mut snap = NbboSnapshot::default();
3149 snap.nbbo_bid_px_1e9 = 67_270_000_000_000;
3150 snap.nbbo_ask_px_1e9 = 67_270_100_000_000;
3151 assert_eq!(snap.nbbo_spread_bps(), 0); let mbps = snap.nbbo_spread_bps_x1000();
3153 assert!(mbps > 0);
3154 assert_eq!(mbps, 14); }
3156
3157 #[test]
3158 fn nbbo_spread_bps_x1000_crossed() {
3159 let mut snap = NbboSnapshot::default();
3160 snap.nbbo_bid_px_1e9 = 67_270_000_000_000;
3161 snap.nbbo_ask_px_1e9 = 67_261_990_000_000;
3162 let mbps = snap.nbbo_spread_bps_x1000();
3163 assert!(mbps < 0, "crossed NBBO should have negative milli-bps");
3164 }
3165
3166 #[test]
3167 fn nbbo_is_crossed() {
3168 let mut snap = NbboSnapshot::default();
3169 snap.venue_ct = 2;
3170 snap.nbbo_bid_px_1e9 = 99_000_000_000;
3172 snap.nbbo_ask_px_1e9 = 101_000_000_000;
3173 snap.nbbo_bid_venue = 0;
3174 snap.nbbo_ask_venue = 1;
3175 assert!(!snap.is_crossed());
3176
3177 snap.nbbo_bid_px_1e9 = 101_500_000_000;
3179 snap.nbbo_ask_px_1e9 = 101_000_000_000;
3180 assert!(snap.is_crossed());
3181
3182 snap.nbbo_bid_venue = 1;
3184 snap.nbbo_ask_venue = 1;
3185 assert!(!snap.is_crossed());
3186 }
3187
3188 #[test]
3189 fn nbbo_venue_staleness() {
3190 let mut snap = NbboSnapshot::default();
3191 snap.venue_ct = 3;
3192 snap.venue_update_ms = [0, 100, 6000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
3193
3194 assert!(!snap.is_venue_stale(0, 5000)); assert!(!snap.is_venue_stale(1, 5000)); assert!(snap.is_venue_stale(2, 5000)); assert!(snap.is_venue_stale(15, 5000)); }
3199
3200 #[test]
3201 fn actions_venue_targeted() {
3202 let mut a = Actions::new();
3203 assert!(a.buy_on(5, 1, 100, 50_000));
3204 assert!(a.sell_on(10, 2, 200, 60_000));
3205 assert!(a.ioc_buy_on(5, 3, 50, 49_000));
3206 assert!(a.ioc_sell_on(10, 4, 75, 61_000));
3207 assert_eq!(a.len(), 4);
3208
3209 let b = a.get(0).unwrap();
3210 assert_eq!(b.venue_id, 5);
3211 assert_eq!(b.side, 1);
3212 assert_eq!(b.order_type, OrderType::LIMIT);
3213
3214 let s = a.get(1).unwrap();
3215 assert_eq!(s.venue_id, 10);
3216 assert_eq!(s.side, -1);
3217
3218 let ib = a.get(2).unwrap();
3219 assert_eq!(ib.order_type, OrderType::IOC);
3220 assert_eq!(ib.venue_id, 5);
3221
3222 let is = a.get(3).unwrap();
3223 assert_eq!(is.order_type, OrderType::IOC);
3224 assert_eq!(is.venue_id, 10);
3225 }
3226
3227 #[test]
3230 fn venue_id_constants_are_correct() {
3231 assert_eq!(VENUE_KRAKEN, 1);
3233 assert_eq!(VENUE_COINBASE, 2);
3234 assert_eq!(VENUE_BINANCE, 3);
3235 assert_eq!(VENUE_BITGET, 4);
3236 assert_eq!(VENUE_CRYPTOCOM, 5);
3237 assert_eq!(VENUE_BITMART, 6);
3238 assert_eq!(VENUE_DEX, 7);
3239 assert_eq!(VENUE_OKX, 8);
3240 assert_eq!(VENUE_BYBIT, 9);
3241 assert_eq!(VENUE_UNKNOWN, 10);
3242 assert_eq!(VENUE_DEX_ETH, 11);
3243 assert_eq!(VENUE_DEX_ARB, 12);
3244 assert_eq!(VENUE_DEX_BASE, 13);
3245 assert_eq!(VENUE_DEX_OP, 14);
3246 assert_eq!(VENUE_DEX_POLY, 15);
3247 assert_eq!(VENUE_DEX_SOL, 16);
3248 }
3249
3250 #[test]
3251 fn is_dex_classification() {
3252 assert!(is_dex(VENUE_DEX));
3253 assert!(is_dex(VENUE_DEX_ETH));
3254 assert!(is_dex(VENUE_DEX_ARB));
3255 assert!(is_dex(VENUE_DEX_BASE));
3256 assert!(is_dex(VENUE_DEX_OP));
3257 assert!(is_dex(VENUE_DEX_POLY));
3258 assert!(is_dex(VENUE_DEX_SOL));
3259 assert!(!is_dex(VENUE_KRAKEN));
3260 assert!(!is_dex(VENUE_BINANCE));
3261 assert!(!is_dex(VENUE_OKX));
3262 assert!(!is_dex(VENUE_UNKNOWN));
3263 assert!(!is_dex(0)); }
3265
3266 #[test]
3267 fn is_cex_classification() {
3268 assert!(is_cex(VENUE_KRAKEN));
3269 assert!(is_cex(VENUE_COINBASE));
3270 assert!(is_cex(VENUE_BINANCE));
3271 assert!(is_cex(VENUE_BITGET));
3272 assert!(is_cex(VENUE_CRYPTOCOM));
3273 assert!(is_cex(VENUE_BITMART));
3274 assert!(is_cex(VENUE_OKX));
3275 assert!(is_cex(VENUE_BYBIT));
3276 assert!(!is_cex(VENUE_DEX));
3277 assert!(!is_cex(VENUE_DEX_ARB));
3278 assert!(!is_cex(VENUE_UNKNOWN));
3279 assert!(!is_cex(0)); }
3281
3282 #[test]
3283 fn venue_name_all_known() {
3284 assert_eq!(venue_name(VENUE_KRAKEN), "kraken");
3285 assert_eq!(venue_name(VENUE_COINBASE), "coinbase");
3286 assert_eq!(venue_name(VENUE_BINANCE), "binance");
3287 assert_eq!(venue_name(VENUE_DEX), "dex");
3288 assert_eq!(venue_name(VENUE_DEX_ARB), "dex-arb");
3289 assert_eq!(venue_name(VENUE_DEX_BASE), "dex-base");
3290 assert_eq!(venue_name(VENUE_DEX_SOL), "dex-sol");
3291 assert_eq!(venue_name(VENUE_UNKNOWN), "unknown");
3292 assert_eq!(venue_name(0), "default");
3293 assert_eq!(venue_name(255), "unknown");
3294 }
3295
3296 #[test]
3297 fn venue_id_cc_wire_mapping() {
3298 assert_eq!(VENUE_KRAKEN, 0 + 1);
3303 assert_eq!(VENUE_COINBASE, 1 + 1);
3304 assert_eq!(VENUE_BINANCE, 2 + 1);
3305 assert_eq!(VENUE_BITGET, 3 + 1);
3306 assert_eq!(VENUE_CRYPTOCOM, 4 + 1);
3307 assert_eq!(VENUE_BITMART, 5 + 1);
3308 assert_eq!(VENUE_DEX, 6 + 1);
3309 assert_eq!(VENUE_OKX, 7 + 1);
3310 assert_eq!(VENUE_BYBIT, 8 + 1);
3311 assert_eq!(VENUE_UNKNOWN, 9 + 1);
3312 assert_eq!(VENUE_DEX_ETH, 10 + 1);
3313 assert_eq!(VENUE_DEX_ARB, 11 + 1);
3314 assert_eq!(VENUE_DEX_BASE, 12 + 1);
3315 assert_eq!(VENUE_DEX_OP, 13 + 1);
3316 assert_eq!(VENUE_DEX_POLY, 14 + 1);
3317 assert_eq!(VENUE_DEX_SOL, 15 + 1);
3318
3319 assert!(!is_cex(VENUE_UNKNOWN));
3321 assert!(!is_dex(VENUE_UNKNOWN));
3322 assert_eq!(venue_name(VENUE_UNKNOWN), "unknown");
3323 }
3324
3325 #[test]
3328 fn abi_venue_books_layout() {
3329 let size = core::mem::size_of::<VenueBooks>();
3330 assert_eq!(size, 13808, "VenueBooks size changed — ABI break!");
3332
3333 assert_eq!(core::mem::align_of::<VenueBooks>(), 8);
3335 }
3336
3337 #[test]
3338 fn venue_books_book_for_venue() {
3339 let mut vb = VenueBooks::default();
3340 vb.book_ct = 3;
3341 vb.venue_ids[0] = VENUE_KRAKEN;
3342 vb.venue_ids[1] = VENUE_COINBASE;
3343 vb.venue_ids[2] = VENUE_DEX_ARB;
3344
3345 vb.books[0].bid_ct = 1;
3347 vb.books[0].bids[0].px_1e9 = 100_000_000_000;
3348 vb.books[1].bid_ct = 2;
3349 vb.books[1].bids[0].px_1e9 = 100_100_000_000;
3350 vb.books[2].bid_ct = 3;
3351 vb.books[2].bids[0].px_1e9 = 99_900_000_000;
3352
3353 let kraken = vb.book_for_venue(VENUE_KRAKEN).unwrap();
3355 assert_eq!(kraken.bids[0].px_1e9, 100_000_000_000);
3356 assert_eq!(kraken.bid_ct, 1);
3357
3358 let coinbase = vb.book_for_venue(VENUE_COINBASE).unwrap();
3359 assert_eq!(coinbase.bids[0].px_1e9, 100_100_000_000);
3360 assert_eq!(coinbase.bid_ct, 2);
3361
3362 let dex = vb.book_for_venue(VENUE_DEX_ARB).unwrap();
3363 assert_eq!(dex.bids[0].px_1e9, 99_900_000_000);
3364 assert_eq!(dex.bid_ct, 3);
3365
3366 assert!(vb.book_for_venue(VENUE_BINANCE).is_none());
3368 }
3369
3370 #[test]
3371 fn venue_books_has_depth_for() {
3372 let mut vb = VenueBooks::default();
3373 vb.book_ct = 2;
3374 vb.venue_ids[0] = VENUE_KRAKEN;
3375 vb.venue_ids[1] = VENUE_DEX_ARB;
3376 vb.books[0].bid_ct = 5; assert!(vb.has_depth_for(VENUE_KRAKEN));
3380 assert!(!vb.has_depth_for(VENUE_DEX_ARB));
3381 assert!(!vb.has_depth_for(VENUE_BINANCE)); }
3383
3384 #[test]
3385 fn venue_books_cex_dex_counts() {
3386 let mut vb = VenueBooks::default();
3387 vb.book_ct = 5;
3388 vb.venue_ids[0] = VENUE_KRAKEN;
3389 vb.venue_ids[1] = VENUE_COINBASE;
3390 vb.venue_ids[2] = VENUE_DEX_ARB;
3391 vb.venue_ids[3] = VENUE_DEX_BASE;
3392 vb.venue_ids[4] = VENUE_BINANCE;
3393
3394 assert_eq!(vb.cex_count(), 3);
3395 assert_eq!(vb.dex_count(), 2);
3396 }
3397
3398 #[test]
3399 fn venue_books_book_at_slot() {
3400 let mut vb = VenueBooks::default();
3401 vb.book_ct = 2;
3402 vb.venue_ids[0] = VENUE_KRAKEN;
3403 vb.venue_ids[1] = VENUE_BINANCE;
3404 vb.books[0].bid_ct = 10;
3405 vb.books[1].bid_ct = 15;
3406
3407 assert_eq!(vb.book_at_slot(0).bid_ct, 10);
3408 assert_eq!(vb.book_at_slot(1).bid_ct, 15);
3409 }
3410
3411 #[test]
3412 fn venue_books_venue_id_at() {
3413 let mut vb = VenueBooks::default();
3414 vb.book_ct = 2;
3415 vb.venue_ids[0] = VENUE_KRAKEN;
3416 vb.venue_ids[1] = VENUE_DEX_ETH;
3417
3418 assert_eq!(vb.venue_id_at(0), VENUE_KRAKEN);
3419 assert_eq!(vb.venue_id_at(1), VENUE_DEX_ETH);
3420 assert_eq!(vb.venue_id_at(99), 0); }
3422
3423 #[test]
3426 fn pool_meta_is_40_bytes() {
3427 assert_eq!(core::mem::size_of::<PoolMeta>(), 40);
3429 }
3430
3431 #[test]
3432 fn pool_books_repr_c_layout() {
3433 let pb = PoolBooks::default();
3435 assert_eq!(pb.pool_ct, 0);
3436 assert_eq!(pb.metas[0].address, [0; 32]);
3437 assert_eq!(pb.books[0].bid_ct, 0);
3438 }
3439
3440 #[test]
3441 fn pool_books_wasm_offset_after_venue_books() {
3442 let venue_end = VENUE_BOOKS_WASM_OFFSET as usize + core::mem::size_of::<VenueBooks>();
3443 assert!(
3444 (POOL_BOOKS_WASM_OFFSET as usize) >= venue_end,
3445 "PoolBooks offset {:#x} would overlap VenueBooks ending at {:#x}",
3446 POOL_BOOKS_WASM_OFFSET,
3447 venue_end,
3448 );
3449 }
3450
3451 #[test]
3452 fn pool_books_book_for_pool() {
3453 let mut pb = PoolBooks::default();
3454 pb.pool_ct = 2;
3455 pb.metas[0].address = [0x11; 32];
3456 pb.metas[0].pair_index = 0;
3457 pb.books[0].bid_ct = 3;
3458
3459 pb.metas[1].address = [0x22; 32];
3460 pb.metas[1].pair_index = 2;
3461 pb.books[1].ask_ct = 5;
3462
3463 let addr1 = [0x11u8; 32];
3464 let book1 = pb.book_for_pool(&addr1, 0).unwrap();
3465 assert_eq!(book1.bid_ct, 3);
3466
3467 let addr2 = [0x22u8; 32];
3468 let book2 = pb.book_for_pool(&addr2, 2).unwrap();
3469 assert_eq!(book2.ask_ct, 5);
3470
3471 assert!(pb.book_for_pool(&addr2, 0).is_none());
3473 let unknown = [0xFF; 32];
3475 assert!(pb.book_for_pool(&unknown, 0).is_none());
3476 }
3477
3478 #[test]
3481 fn online_features_size_is_256() {
3482 assert_eq!(core::mem::size_of::<OnlineFeatures>(), 256);
3483 }
3484
3485 #[test]
3486 fn online_features_default_version() {
3487 let f = OnlineFeatures::default();
3488 assert_eq!(f.version, 1);
3489 assert_eq!(f.flags, 0);
3490 assert_eq!(f.microprice_1e9, 0);
3491 assert_eq!(f.feature_ts_ns, 0);
3492 }
3493
3494 #[test]
3495 fn online_features_vpin_flag() {
3496 let mut f = OnlineFeatures::default();
3497 assert!(!f.vpin_valid());
3498 f.flags = 1;
3499 assert!(f.vpin_valid());
3500 f.flags = 0b11;
3501 assert!(f.vpin_valid());
3502 f.flags = 0b10;
3503 assert!(!f.vpin_valid());
3504 }
3505
3506 #[test]
3507 fn online_features_wasm_offset_after_pool_books() {
3508 let pool_end = POOL_BOOKS_WASM_OFFSET as usize + core::mem::size_of::<PoolBooks>();
3509 assert!(
3510 (ONLINE_FEATURES_WASM_OFFSET as usize) >= pool_end,
3511 "OnlineFeatures offset {:#x} would overlap PoolBooks ending at {:#x}",
3512 ONLINE_FEATURES_WASM_OFFSET,
3513 pool_end,
3514 );
3515 }
3516
3517 #[test]
3518 fn online_features_helpers() {
3519 let mut f = OnlineFeatures::default();
3520 f.microprice_1e9 = 50_000_000_000_000; assert!((f.microprice_f64() - 50_000.0).abs() < 0.001);
3522
3523 f.ofi_1level_1e8 = 150_000_000; assert!((f.ofi_1level_f64() - 1.5).abs() < 0.001);
3525
3526 f.trade_sign_imbalance_1e6 = -500_000; assert!((f.trade_sign_imbalance_f64() - (-0.5)).abs() < 0.001);
3528 }
3529
3530 #[test]
3531 fn online_features_is_copy() {
3532 let f = OnlineFeatures::default();
3533 let g = f; assert_eq!(g.version, f.version);
3535 }
3536}