off_grid_primitives/spot/event.rs
1// core_events/src/lib.rs
2use once_cell::sync::OnceCell;
3use std::sync::{mpsc, Mutex};
4use std::thread;
5use std::fmt;
6use serde::{Serialize, Deserialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum SpotEvent {
10 /// New market price event in Spot
11 SpotNewMarketPrice {
12 /// client id
13 /// multiple client ids can be provided for the same event
14 client_ids: Vec<Vec<u8>>,
15 /// pair id
16 #[serde(with = "serde_bytes")]
17 pair_id: Vec<u8>,
18 /// base asset id
19 #[serde(with = "serde_bytes")]
20 base_asset_id: Vec<u8>,
21 /// quote asset id
22 #[serde(with = "serde_bytes")]
23 quote_asset_id: Vec<u8>,
24 /// price
25 price: u64,
26 /// timestamp in milliseconds
27 /// i64 is chosen because of js type compatibility
28 timestamp: i64,
29 },
30 /// Spot pair rebate happened when maker fee bps is lower than 0
31 SpotPairRebate {
32 /// pair id
33 #[serde(with = "serde_bytes")]
34 pair_id: Vec<u8>,
35 /// base asset id
36 #[serde(with = "serde_bytes")]
37 base_asset_id: Vec<u8>,
38 /// quote asset id
39 #[serde(with = "serde_bytes")]
40 quote_asset_id: Vec<u8>,
41 /// price
42 price: u64,
43 /// is bid
44 is_bid: bool,
45 /// account id
46 #[serde(with = "serde_bytes")]
47 account_id: Vec<u8>,
48 /// happened on maker/taker side
49 is_maker: bool,
50 /// fee bps
51 fee_bps: i16,
52 /// asset volume
53 base_volume: u64,
54 /// quote volume
55 quote_volume: u64,
56 /// timestamp
57 timestamp: i64,
58 },
59 /// Spot pair client account changed
60 SpotPairClientAccountChanged {
61 /// pair id
62 #[serde(with = "serde_bytes")]
63 pair_id: Vec<u8>,
64 /// client id
65 #[serde(with = "serde_bytes")]
66 cid: Option<Vec<u8>>,
67 /// admin account id
68 #[serde(with = "serde_bytes")]
69 admin_account_id: Option<Vec<u8>>,
70 /// fee account id
71 #[serde(with = "serde_bytes")]
72 fee_account_id: Option<Vec<u8>>,
73 /// timestamp
74 /// i64 is chosen because of js type compatibility
75 timestamp: i64,
76 },
77 /// Pair added to the matching engine
78 SpotPairAdded {
79 /// client id
80 #[serde(with = "serde_bytes")]
81 cid: Vec<u8>,
82 /// pair id
83 #[serde(with = "serde_bytes")]
84 pair_id: Vec<u8>,
85 /// timestamp
86 /// i64 is chosen because of js type compatibility
87 timestamp: i64,
88 },
89 /// Transfer event from an account to another account
90 Transfer {
91 /// client id
92 #[serde(with = "serde_bytes")]
93 cid: Vec<u8>,
94 /// from account id
95 #[serde(with = "serde_bytes")]
96 from: Vec<u8>,
97 /// to account id
98 #[serde(with = "serde_bytes")]
99 to: Vec<u8>,
100 /// asset id
101 #[serde(with = "serde_bytes")]
102 asset: Vec<u8>,
103 /// amount
104 amnt: u64,
105 /// timestamp
106 timestamp: i64,
107 },
108 /// Spot order block changed in the orderbook
109 SpotPriceLevelChanged {
110 /// pair id
111 #[serde(with = "serde_bytes")]
112 pair_id: Vec<u8>,
113 /// is bid
114 is_bid: bool,
115 /// price
116 price: u64,
117 /// public quantity
118 pqty: u64,
119 /// current quantity
120 cqty: u64,
121 /// timestamp
122 timestamp: i64,
123 },
124 /// Spot order placed in the orderbook being a maker
125 SpotOrderPlaced {
126 /// client id
127 #[serde(with = "serde_bytes")]
128 cid: Vec<u8>,
129 /// pair id
130 #[serde(with = "serde_bytes")]
131 pair_id: Vec<u8>,
132 /// base asset id
133 #[serde(with = "serde_bytes")]
134 base_asset_id: Vec<u8>,
135 /// quote asset id
136 #[serde(with = "serde_bytes")]
137 quote_asset_id: Vec<u8>,
138 /// order id
139 #[serde(with = "serde_bytes")]
140 order_id: Vec<u8>,
141 /// maker account id
142 #[serde(with = "serde_bytes")]
143 maker_account_id: Vec<u8>,
144 /// maker order is bid
145 is_bid: bool,
146 /// price
147 price: u64,
148 /// whole amount
149 amnt: u64,
150 /// iceberg quantity
151 iqty: u64,
152 /// current quantity
153 cqty: u64,
154 /// public quantity
155 pqty: u64,
156 /// timestamp, i64 is chosen because of js type compatibility
157 timestamp: i64,
158 /// expires at timestamp, i64 is chosen because of js type compatibility
159 expires_at: i64
160 },
161 /// Spot order partially filled in the orderbook being a taker for taker spot order history
162 SpotOrderPartiallyFilled {
163 /// whether the event is for a taker order
164 is_taker_event: bool,
165 /// taker client id
166 #[serde(with = "serde_bytes")]
167 taker_cid: Vec<u8>,
168 /// maker client id
169 #[serde(with = "serde_bytes")]
170 maker_cid: Vec<u8>,
171 /// taker order id
172 #[serde(with = "serde_bytes")]
173 taker_order_id: Vec<u8>,
174 /// maker order id
175 #[serde(with = "serde_bytes")]
176 maker_order_id: Vec<u8>,
177 /// maker account id
178 #[serde(with = "serde_bytes")]
179 taker_account_id: Vec<u8>,
180 /// taker account id
181 #[serde(with = "serde_bytes")]
182 maker_account_id: Vec<u8>,
183 /// taker order is bid
184 taker_order_is_bid: bool,
185 /// maker order is bid
186 maker_order_is_bid: bool,
187 /// price
188 price: u64,
189 /// pair id
190 #[serde(with = "serde_bytes")]
191 pair_id: Vec<u8>,
192 /// base asset id
193 #[serde(with = "serde_bytes")]
194 base_asset_id: Vec<u8>,
195 /// quote asset id
196 #[serde(with = "serde_bytes")]
197 quote_asset_id: Vec<u8>,
198 /// base volume
199 base_volume: u64,
200 /// quote volume
201 quote_volume: u64,
202 /// base fee
203 base_fee: u64,
204 /// quote fee
205 quote_fee: u64,
206 /// maker fee bps
207 maker_fee_bps: i16,
208 /// taker fee bps
209 taker_fee_bps: i16,
210 /// whole amount
211 amnt: u64,
212 /// iceberg quantity
213 iqty: u64,
214 /// public quantity
215 pqty: u64,
216 /// current quantity
217 cqty: u64,
218 /// timestamp, i64 is chosen because of js type compatibility
219 timestamp: i64,
220 /// expires at timestamp, i64 is chosen because of js type compatibility
221 expires_at: i64
222 },
223 /// Spot order fully filled in the orderbook being a taker for taker spot order history
224 SpotOrderFullyFilled {
225 /// whether the event is for a taker order
226 is_taker_event: bool,
227 /// taker client id
228 #[serde(with = "serde_bytes")]
229 taker_cid: Vec<u8>,
230 /// maker client id
231 #[serde(with = "serde_bytes")]
232 maker_cid: Vec<u8>,
233 /// taker order id
234 #[serde(with = "serde_bytes")]
235 taker_order_id: Vec<u8>,
236 /// maker order id
237 #[serde(with = "serde_bytes")]
238 maker_order_id: Vec<u8>,
239 /// maker account id
240 #[serde(with = "serde_bytes")]
241 maker_account_id: Vec<u8>,
242 /// taker account id
243 #[serde(with = "serde_bytes")]
244 taker_account_id: Vec<u8>,
245 /// taker order is bid
246 taker_order_is_bid: bool,
247 /// maker order is bid
248 maker_order_is_bid: bool,
249 /// price
250 price: u64,
251 /// pair id
252 #[serde(with = "serde_bytes")]
253 pair_id: Vec<u8>,
254 /// base asset id
255 #[serde(with = "serde_bytes")]
256 base_asset_id: Vec<u8>,
257 /// quote asset id
258 #[serde(with = "serde_bytes")]
259 quote_asset_id: Vec<u8>,
260 /// base volume
261 base_volume: u64,
262 /// quote volume
263 quote_volume: u64,
264 /// base fee
265 base_fee: u64,
266 /// quote fee
267 quote_fee: u64,
268 /// maker fee bps
269 maker_fee_bps: i16,
270 /// taker fee bps
271 taker_fee_bps: i16,
272 /// whole amount
273 amnt: u64,
274 /// iceberg quantity
275 iqty: u64,
276 /// public quantity
277 pqty: u64,
278 /// current quantity
279 cqty: u64,
280 /// timestamp, i64 is chosen because of js type compatibility
281 timestamp: i64,
282 /// expires at timestamp, i64 is chosen because of js type compatibility
283 expires_at: i64
284 },
285 /// Spot order cancelled in the orderbook regardless of being a maker or taker
286 SpotOrderCancelled {
287 /// client id
288 #[serde(with = "serde_bytes")]
289 cid: Vec<u8>,
290 /// order id
291 #[serde(with = "serde_bytes")]
292 order_id: Vec<u8>,
293 /// maker account id
294 #[serde(with = "serde_bytes")]
295 maker_account_id: Vec<u8>,
296 /// is bid
297 is_bid: bool,
298 /// price
299 price: u64,
300 /// whole amount
301 amnt: u64,
302 /// iceberg quantity
303 iqty: u64,
304 /// public quantity
305 pqty: u64,
306 /// current quantity
307 cqty: u64,
308 /// timestamp, i64 is chosen because of js type compatibility
309 timestamp: i64,
310 /// expires at timestamp, i64 is chosen because of js type compatibility
311 expires_at: i64
312 },
313 /// Spot order expired in the orderbook regardless of being a maker
314 SpotOrderExpired {
315 /// client id
316 #[serde(with = "serde_bytes")]
317 cid: Vec<u8>,
318 /// order id
319 #[serde(with = "serde_bytes")]
320 order_id: Vec<u8>,
321 /// maker account id
322 #[serde(with = "serde_bytes")]
323 maker_account_id: Vec<u8>,
324 /// is bid
325 is_bid: bool,
326 /// price
327 price: u64,
328 /// whole amount
329 amnt: u64,
330 /// iceberg quantity
331 iqty: u64,
332 /// public quantity
333 pqty: u64,
334 /// current quantity
335 cqty: u64,
336 /// timestamp, i64 is chosen because of js type compatibility
337 timestamp: i64,
338 /// expires at timestamp, i64 is chosen because of js type compatibility
339 expires_at: i64
340 },
341 SpotOrderIcebergQuantityChanged {
342 /// client id
343 #[serde(with = "serde_bytes")]
344 cid: Vec<u8>,
345 /// order id
346 #[serde(with = "serde_bytes")]
347 order_id: Vec<u8>,
348 /// whole initial amount
349 amnt: u64,
350 /// iceberg quantity
351 iqty: u64,
352 /// public quantity
353 pqty: u64,
354 /// current quantity
355 cqty: u64,
356 /// timestamp, i64 is chosen because of js type compatibility
357 timestamp: i64,
358 /// expires at timestamp, i64 is chosen because of js type compatibility
359 expires_at: i64
360 }
361}
362
363/// A queue of events that can be formatted and displayed.
364/// This is a newtype wrapper around `Vec<SpotEvent>` that provides better formatting support.
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(transparent)]
367pub struct EventQueue(pub Vec<SpotEvent>);
368
369impl EventQueue {
370 /// Create a new empty event queue
371 pub fn new() -> Self {
372 EventQueue(Vec::new())
373 }
374
375 /// Create an event queue from a vector of events
376 pub fn from_vec(events: Vec<SpotEvent>) -> Self {
377 EventQueue(events)
378 }
379
380 /// Get a reference to the underlying vector
381 pub fn as_vec(&self) -> &Vec<SpotEvent> {
382 &self.0
383 }
384
385 /// Consume the wrapper and return the underlying vector
386 pub fn into_vec(self) -> Vec<SpotEvent> {
387 self.0
388 }
389
390 /// Check if the queue is empty
391 pub fn is_empty(&self) -> bool {
392 self.0.is_empty()
393 }
394
395 /// Get the number of events in the queue
396 pub fn len(&self) -> usize {
397 self.0.len()
398 }
399
400 /// Log all events in this queue using `println!`.
401 ///
402 /// This is primarily intended for debugging or tests. In production code,
403 /// prefer sending events to a structured logging backend.
404 pub fn log(&self, prefix: &str) {
405 if self.0.is_empty() {
406 println!("{prefix}[]");
407 return;
408 }
409
410 for (i, event) in self.0.iter().enumerate() {
411 println!("{prefix}[{i}]: {:?}", event);
412 }
413 }
414}
415
416impl Default for EventQueue {
417 fn default() -> Self {
418 EventQueue::new()
419 }
420}
421
422impl From<Vec<SpotEvent>> for EventQueue {
423 fn from(events: Vec<SpotEvent>) -> Self {
424 EventQueue(events)
425 }
426}
427
428impl From<EventQueue> for Vec<SpotEvent> {
429 fn from(queue: EventQueue) -> Self {
430 queue.0
431 }
432}
433
434impl fmt::Display for EventQueue {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 if self.0.is_empty() {
437 return write!(f, "[]");
438 }
439
440 write!(f, "[")?;
441 for (i, event) in self.0.iter().enumerate() {
442 if i > 0 {
443 write!(f, ", ")?;
444 }
445 write!(f, "{:?}", event)?;
446 }
447 write!(f, "]")
448 }
449}
450
451impl std::ops::Deref for EventQueue {
452 type Target = Vec<SpotEvent>;
453
454 fn deref(&self) -> &Self::Target {
455 &self.0
456 }
457}
458
459impl std::ops::DerefMut for EventQueue {
460 fn deref_mut(&mut self) -> &mut Self::Target {
461 &mut self.0
462 }
463}
464
465pub trait EventBackend: Send + 'static {
466 fn handle_event(&mut self, event: SpotEvent);
467}
468
469// Sender into the dispatcher
470static DISPATCH_TX: OnceCell<mpsc::Sender<SpotEvent>> = OnceCell::new();
471
472// List of per-backend senders
473static BACKEND_TXS: OnceCell<Mutex<Vec<mpsc::Sender<SpotEvent>>>> = OnceCell::new();
474
475// In-memory event queue that stores events before they are published
476static EVENT_QUEUE: OnceCell<Mutex<Vec<SpotEvent>>> = OnceCell::new();
477
478fn backend_txs() -> &'static Mutex<Vec<mpsc::Sender<SpotEvent>>> {
479 BACKEND_TXS.get_or_init(|| Mutex::new(Vec::new()))
480}
481
482fn event_queue() -> &'static Mutex<Vec<SpotEvent>> {
483 EVENT_QUEUE.get_or_init(|| Mutex::new(Vec::new()))
484}
485
486/// Call once at process startup to create the dispatcher thread.
487pub fn init_event_bus() {
488 let (tx, rx) = mpsc::channel::<SpotEvent>();
489 DISPATCH_TX.set(tx).ok(); // ignore if already set
490
491 // Dispatcher thread: fan out every event to all registered backends.
492 thread::spawn(move || {
493 while let Ok(event) = rx.recv() {
494 // clone once per backend
495 let backends = backend_txs().lock().unwrap();
496 for backend_tx in backends.iter() {
497 // Ignore send errors (backend might have shut down)
498 let _ = backend_tx.send(event.clone());
499 }
500 }
501 });
502}
503
504/// Called from anywhere (engine, core logic) to emit an event.
505/// This stores the event in the event queue. Use `publish_events()` to actually send them.
506pub fn emit_event(event: SpotEvent) {
507 let mut queue = event_queue().lock().unwrap();
508 queue.push(event);
509}
510
511/// Publishes all events from the global queue to the event bus (if initialized).
512/// After publishing, the queue is drained and cleared.
513pub fn publish_events() {
514 // Drain all events from the queue
515 let events: Vec<SpotEvent> = {
516 let mut queue = event_queue().lock().unwrap();
517 let drained = queue.clone();
518 queue.clear();
519 drained
520 };
521
522 // Send each event to the dispatcher if it's initialized
523 if let Some(tx) = DISPATCH_TX.get() {
524 for event in events {
525 // ignore error if dispatcher is down
526 let _ = tx.send(event);
527 }
528 }
529}
530
531/// Publishes an EventQueue to the event bus (if initialized).
532/// This is useful when you have an EventQueue returned from an operation.
533pub fn publish_event_queue(events: EventQueue) {
534 // Send each event to the dispatcher if it's initialized
535 if let Some(tx) = DISPATCH_TX.get() {
536 for event in events.into_vec() {
537 // ignore error if dispatcher is down
538 let _ = tx.send(event);
539 }
540 }
541}
542
543/// Register a backend; returns an `mpsc::Receiver<SpotEvent>` that you
544/// can consume from a dedicated thread.
545pub fn register_backend() -> mpsc::Receiver<SpotEvent> {
546 let (tx, rx) = mpsc::channel::<SpotEvent>();
547
548 {
549 let mut list = backend_txs().lock().unwrap();
550 list.push(tx);
551 }
552
553 rx
554}
555
556/// Drains all events from the event queue and returns them.
557/// This clears the queue after draining.
558/// Useful for retrieving events after operations complete.
559pub fn drain_events() -> EventQueue {
560 let mut queue = event_queue().lock().unwrap();
561 let drained = queue.clone();
562 queue.clear();
563 EventQueue(drained)
564}
565
566/// Clears all events from the event queue without returning them.
567pub fn clear_events() {
568 let mut queue = event_queue().lock().unwrap();
569 queue.clear();
570}