Skip to main content

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}