algo_sdk/events.rs
1//! Execution events — fills, rejects, extended metadata.
2
3// =============================================================================
4// FILL EVENT
5// =============================================================================
6
7#[derive(Debug, Clone, Copy)]
8#[repr(C)]
9pub struct Fill {
10 pub order_id: u64,
11 pub px_1e9: u64,
12 pub qty_1e8: i64,
13 pub recv_ns: u64, // Timestamp when fill was received
14 pub side: i8,
15 pub _pad: [u8; 7],
16}
17
18impl Fill {
19 /// Elapsed time since a caller-provided start timestamp.
20 /// Typical use: `fill.since_ms(start_ns)` where `start_ns` came from `book.recv_ns`.
21 #[inline(always)]
22 pub fn since_ns(&self, start_ns: u64) -> u64 {
23 self.recv_ns.saturating_sub(start_ns)
24 }
25
26 #[inline(always)]
27 pub fn since_us(&self, start_ns: u64) -> u64 {
28 self.since_ns(start_ns) / 1_000
29 }
30
31 #[inline(always)]
32 pub fn since_ms(&self, start_ns: u64) -> u64 {
33 self.since_ns(start_ns) / 1_000_000
34 }
35
36 /// Symbol identifier (v0.4+). 0 = single-symbol or unset.
37 /// In multi-symbol backtests, matches L2Book.symbol_id.
38 /// Stored in _pad[0..2] as little-endian u16 for ABI backward compat.
39 #[inline(always)]
40 pub fn symbol_id(&self) -> u16 {
41 u16::from_le_bytes([self._pad[0], self._pad[1]])
42 }
43
44 /// Set symbol identifier. Used by sim-engine orchestrator.
45 #[inline(always)]
46 pub fn set_symbol_id(&mut self, id: u16) {
47 let bytes = id.to_le_bytes();
48 self._pad[0] = bytes[0];
49 self._pad[1] = bytes[1];
50 }
51}
52
53// ABI size assertions — these must match the WASM memory layout expectations
54const _: () = assert!(
55 core::mem::size_of::<Fill>() == 40,
56 "Fill size changed — ABI break"
57);
58const _: () = assert!(
59 core::mem::size_of::<Reject>() == 16,
60 "Reject size changed — ABI break"
61);
62
63// =============================================================================
64// FILL EXTENDED METADATA
65// =============================================================================
66
67/// Extended fill metadata (NOT in WASM memory — delivered via separate channel).
68///
69/// Provides additional context about a fill that doesn't fit in the fixed-size
70/// `Fill` struct in WASM shared memory. Delivered out-of-band by the runtime.
71#[derive(Debug, Clone, Copy, Default)]
72pub struct FillExt {
73 /// 1 = maker, 0 = taker, 255 = unknown.
74 pub is_maker: u8,
75 /// Fee in 1e9 units. Negative = rebate.
76 pub fee_1e9: i64,
77 /// Estimated queue depth ahead at time of fill (1e8 units), or -1 if unknown.
78 pub queue_ahead_1e8: i64,
79}
80
81// =============================================================================
82// REJECT EVENT
83// =============================================================================
84
85/// Reject codes from exchange (matches cc_proto::RejectClass).
86pub mod RejectCode {
87 /// Unknown error
88 pub const UNKNOWN: u8 = 0;
89 /// Insufficient balance/funds
90 pub const INSUFFICIENT_BALANCE: u8 = 1;
91 /// Invalid parameters (price, qty, symbol)
92 pub const INVALID_PARAMS: u8 = 2;
93 /// Exchange rate limit hit
94 pub const RATE_LIMIT: u8 = 3;
95 /// Exchange temporarily unavailable
96 pub const EXCHANGE_BUSY: u8 = 4;
97 /// Network error
98 pub const NETWORK: u8 = 5;
99 /// Authentication error (invalid API key/secret)
100 pub const AUTH: u8 = 6;
101
102 // Internal reject codes (from risk engine, >=100)
103 /// Risk check failed
104 pub const RISK: u8 = 100;
105 /// Position limit exceeded
106 pub const POSITION_LIMIT: u8 = 101;
107 /// Kill switch triggered
108 pub const KILL_SWITCH: u8 = 102;
109 /// Fat-finger: price deviates too far from reference (best bid/ask)
110 pub const PRICE_DEVIATION: u8 = 103;
111 /// Daily P&L loss limit breached — algo paused
112 pub const DAILY_LOSS_LIMIT: u8 = 104;
113 /// Venue ID not in registered venue set (multi-venue)
114 pub const BAD_VENUE: u8 = 105;
115 /// Target venue data is stale (multi-venue)
116 pub const STALE_VENUE: u8 = 106;
117
118 /// Get human-readable description for a reject code.
119 pub fn to_str(code: u8) -> &'static str {
120 match code {
121 UNKNOWN => "UNKNOWN",
122 INSUFFICIENT_BALANCE => "INSUFFICIENT_BALANCE",
123 INVALID_PARAMS => "INVALID_PARAMS",
124 RATE_LIMIT => "RATE_LIMIT",
125 EXCHANGE_BUSY => "EXCHANGE_BUSY",
126 NETWORK => "NETWORK",
127 AUTH => "AUTH",
128 RISK => "RISK_CHECK_FAILED",
129 POSITION_LIMIT => "POSITION_LIMIT",
130 KILL_SWITCH => "KILL_SWITCH",
131 PRICE_DEVIATION => "PRICE_DEVIATION",
132 DAILY_LOSS_LIMIT => "DAILY_LOSS_LIMIT",
133 BAD_VENUE => "BAD_VENUE",
134 STALE_VENUE => "STALE_VENUE",
135 _ => "UNKNOWN",
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy)]
141#[repr(C)]
142pub struct Reject {
143 pub order_id: u64,
144 pub code: u8,
145 pub _pad: [u8; 7],
146}
147
148impl Reject {
149 /// Get human-readable description of the reject reason.
150 #[inline]
151 pub fn reason(&self) -> &'static str {
152 RejectCode::to_str(self.code)
153 }
154
155 /// Symbol identifier (v0.4+). 0 = single-symbol or unset.
156 #[inline(always)]
157 pub fn symbol_id(&self) -> u16 {
158 u16::from_le_bytes([self._pad[0], self._pad[1]])
159 }
160
161 #[inline(always)]
162 pub fn set_symbol_id(&mut self, id: u16) {
163 let bytes = id.to_le_bytes();
164 self._pad[0] = bytes[0];
165 self._pad[1] = bytes[1];
166 }
167}