Skip to main content

off_grid_primitives/spot/
orderbook.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::spot::{
6    event::{self, SpotEvent},
7    Order,
8};
9
10use super::{
11    orders::{L3Error, OrderId},
12    prices::L2Error,
13    L2, L3,
14};
15
16/// In-memory order book for spot markets.
17/// - `l2` is the L2 state of the orderbook.
18/// - `l3` is the L3 state of the orderbook.
19/// - `fee_recipients` is the map of client id to fee recipient account id.
20/// - `dust` is the dust limit to determine if the order should be deleted.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
22pub struct OrderBook {
23    // L2 state
24    pub l2: L2,
25    // L3 state
26    pub l3: L3,
27    // Fee recipients map where key is the client id, and value is the fee recipient account id
28    pub fee_recipients: HashMap<Vec<u8>, Vec<u8>>,
29    // dust limit to determine if the order should be deleted
30    pub dust: u64,
31}
32
33#[derive(Debug, thiserror::Error, PartialEq, Eq)]
34pub enum OrderBookError {
35    #[error("price is zero")]
36    PriceIsZero,
37    #[error("amount is zero")]
38    AmountIsZero,
39    #[error("public amount is zero")]
40    PublicAmountIsZero,
41    #[error("order is not owned by the sender")]
42    OrderNotOwnedBySender,
43    #[error("L3 error: {0}")]
44    L3(L3Error),
45    #[error("L2 error: {0}")]
46    L2(L2Error),
47    #[error("iceberg quantity is bigger than whole amount")]
48    IcebergQuantityIsBiggerThanWholeAmount,
49    #[error("order has expired")]
50    OrderExpired,
51    #[error("unsupported time in force is provided")]
52    UnsupportedTimeInForce,
53    #[error("order is not supported by the client id")]
54    OrderNotSupportedByClientId,
55    #[error("fill or kill order not fully filled")]
56    OrderNotFullyFilled,
57    #[error("no ask orders in the orderbook")]
58    NoAskOrdersInOrderbook,
59    #[error("no bid orders in the orderbook")]
60    NoBidOrdersInOrderbook,
61}
62
63impl From<L3Error> for OrderBookError {
64    fn from(err: L3Error) -> Self {
65        OrderBookError::L3(err)
66    }
67}
68
69impl From<L2Error> for OrderBookError {
70    fn from(err: L2Error) -> Self {
71        OrderBookError::L2(err)
72    }
73}
74
75impl OrderBook {
76    pub fn new() -> Self {
77        Self {
78            l2: L2::new(),
79            l3: L3::new(),
80            fee_recipients: HashMap::new(),
81            dust: 1000,
82        }
83    }
84
85    /// Sets the dust limit to determine if the order should be deleted
86    pub fn set_dust(&mut self, dust: u64) {
87        self.dust = dust;
88    }
89
90    /// Gets the required amount to match an order as taker to match with the maker order and clear it.
91    /// - `taker_order` is the taker order.
92    /// - `price` is the price of the maker order.
93    /// - `amount` is the amount of the taker order to match with the maker order.
94    pub fn get_required(
95        &self,
96        taker_order: Order,
97        price: u64,
98        amount: u64,
99    ) -> Result<u64, OrderBookError> {
100        if taker_order.is_bid {
101            Ok(amount.saturating_mul(price).saturating_div(1_0000_0000))
102        } else {
103            Ok(amount.saturating_mul(1_0000_0000).saturating_div(price))
104        }
105    }
106
107    /// clears empty head of the order book where price is in linked list, but order is not in the price level
108    pub fn clear_empty_head(&mut self, is_bid: bool) -> Result<u64, OrderBookError> {
109        // Get the current head price
110        let mut head = if is_bid {
111            self.l2.bid_head()
112        } else {
113            self.l2.ask_head()
114        };
115
116        // While head exists and has no orders, clear it and move to the next head
117        while let Some(head_price) = head {
118            // Check if there are orders at this price level
119            let order_id = self.l3.head(head_price);
120
121            // If there are orders at this price level, we're done
122            if order_id.is_some() {
123                return Ok(head_price);
124            }
125
126            // No orders at this price level, clear the head and move to next
127            head = self.l2.clear_head(is_bid)?;
128        }
129
130        // No head exists (all heads were empty and cleared)
131        Err(OrderBookError::PriceIsZero)
132    }
133
134    /// clears empty head of the order book, returns 0 if no head exists (matches Solidity behavior)
135    pub fn clear_empty_head_or_zero(&mut self, is_bid: bool) -> u64 {
136        self.clear_empty_head(is_bid).unwrap_or(0)
137    }
138
139    /// pop front on the orderbook
140    pub fn pop_front(&mut self, is_bid: bool) -> Result<Order, OrderBookError> {
141        let now = std::time::SystemTime::now()
142            .duration_since(std::time::UNIX_EPOCH)
143            .unwrap()
144            .as_millis() as i64;
145        loop {
146            self.clear_empty_head(is_bid)?;
147            let head = if is_bid {
148                self.l2.bid_head()
149            } else {
150                self.l2.ask_head()
151            };
152
153            let head_price = match head {
154                Some(price) => price,
155                None => continue,
156            };
157            let head_id = self.l3.head(head_price);
158            if let Some(order_id) = head_id {
159                let order = self.l3.get_order(order_id)?;
160                // if the order is expired, expire it and continue
161                if order.expires_at <= now {
162                    self._expire_order(order_id, is_bid, Vec::new(), now)?;
163                    continue;
164                }
165                // if the expired order empties the price level, remove the price level, move to next head and continue
166                if self.l3.is_empty(order.price) {
167                    self.l2.remove_price(is_bid, order.price)?;
168                    continue;
169                }
170                // if the order is not expired, pop it from the orderbook
171                let (order, is_empty) = self.l3.pop_front(head_price)?;
172                if is_empty {
173                    self.l2.clear_head(is_bid)?;
174                }
175                return Ok(order
176                    .expect("head price must have at least one order")
177                    .clone());
178            }
179        }
180    }
181
182    /// Places a bid order.
183    /// - returns the order id and if a dormant order was found.
184    /// - `cid` is the client order id.
185    /// - `owner` is the owner of the order.
186    /// - `price` is the price of the order.
187    /// - `amount` is the amount of the order.
188    /// - `public_amount` is the public amount of the order.
189    /// - `timestamp` is the timestamp of the order.
190    pub fn place_bid(
191        &mut self,
192        cid: impl Into<Vec<u8>>,
193        pair_id: impl Into<Vec<u8>>,
194        base_asset_id: impl Into<Vec<u8>>,
195        quote_asset_id: impl Into<Vec<u8>>,
196        owner: impl Into<Vec<u8>>,
197        price: u64,
198        amnt: u64,
199        iqty: u64,
200        timestamp: i64,
201        expires_at: i64,
202        maker_fee_bps: i16,
203    ) -> Result<Order, OrderBookError> {
204        let cid = cid.into();
205        let pair_id = pair_id.into();
206        let base_asset_id = base_asset_id.into();
207        let quote_asset_id = quote_asset_id.into();
208        let owner = owner.into();
209        let price = price;
210        if iqty > amnt {
211            return Err(OrderBookError::IcebergQuantityIsBiggerThanWholeAmount);
212        }
213        let pqty = amnt - iqty;
214
215        let order = self.l3.create_order(
216            cid.clone(),
217            owner.clone(),
218            true,
219            price,
220            amnt,
221            iqty,
222            timestamp,
223            expires_at,
224            maker_fee_bps,
225        )?;
226
227        // emit the event for the order created
228        event::emit_event(SpotEvent::SpotOrderPlaced {
229            cid: cid.clone(),
230            pair_id: pair_id.clone(),
231            base_asset_id: base_asset_id.clone(),
232            quote_asset_id: quote_asset_id.clone(),
233            order_id: order.id.to_bytes().to_vec(),
234            maker_account_id: owner.into(),
235            is_bid: true,
236            price: price,
237            amnt: amnt,
238            iqty: iqty,
239            pqty: pqty,
240            cqty: amnt,
241            timestamp: timestamp,
242            expires_at: expires_at,
243        });
244
245        // update the price level on the orderbook
246        self.update_price_level(pair_id, true, true, price, pqty, amnt, None)?;
247        Ok(order)
248    }
249
250    /// Places an ask order.
251    /// - returns the order id and if a dormant order was found.
252    /// - `cid` is the client order id.
253    /// - `owner` is the owner of the order.
254    /// - `price` is the price of the order.
255    /// - `amount` is the amount of the order.
256    /// - `public_amount` is the public amount of the order.
257    /// - `timestamp` is the timestamp of the order.
258    pub fn place_ask(
259        &mut self,
260        cid: impl Into<Vec<u8>>,
261        pair_id: impl Into<Vec<u8>>,
262        base_asset_id: impl Into<Vec<u8>>,
263        quote_asset_id: impl Into<Vec<u8>>,
264        owner: impl Into<Vec<u8>>,
265        price: u64,
266        amnt: u64,
267        iqty: u64,
268        timestamp: i64,
269        expires_at: i64,
270        maker_fee_bps: i16,
271    ) -> Result<Order, OrderBookError> {
272        let cid = cid.into();
273        let pair_id = pair_id.into();
274        let base_asset_id = base_asset_id.into();
275        let quote_asset_id = quote_asset_id.into();
276        let owner = owner.into();
277        let price = price.into();
278        let amnt = amnt.into();
279        let iqty = iqty.into();
280        if iqty > amnt {
281            return Err(OrderBookError::IcebergQuantityIsBiggerThanWholeAmount);
282        }
283        let pqty = amnt - iqty;
284
285        let order = self.l3.create_order(
286            cid.clone(),
287            owner.clone(),
288            false,
289            price,
290            amnt,
291            iqty,
292            timestamp,
293            expires_at,
294            maker_fee_bps,
295        )?;
296
297        // emit the event for the order created
298        event::emit_event(SpotEvent::SpotOrderPlaced {
299            cid: cid.clone(),
300            pair_id: pair_id.clone(),
301            base_asset_id: base_asset_id.clone(),
302            quote_asset_id: quote_asset_id.clone(),
303            order_id: order.id.to_bytes().to_vec(),
304            maker_account_id: owner,
305            is_bid: false,
306            price: price,
307            amnt: amnt,
308            iqty: iqty,
309            pqty: pqty,
310            cqty: amnt,
311            timestamp: timestamp,
312            expires_at: expires_at,
313        });
314
315        // update the price level on the orderbook
316        self.update_price_level(pair_id, true, false, price, pqty, amnt, None)?;
317        Ok(order)
318    }
319
320    /// Executes a trade.
321    /// - returns the trade details.
322    /// - `is_bid` is whether the order from client is a bid order.
323    /// - `taker_order` is the taker order.
324    /// - `maker_order` is the maker order.
325    /// - `pair_id` is the pair id.
326    /// - `base_asset_id` is the base asset id.
327    /// - `quote_asset_id` is the quote asset id.
328    /// - `managing_account_id` is the managing account id.
329    /// - `matching_amount` is the amount of the taker order to match with the maker order.
330    /// - `clear` is whether to clear the order.
331    pub fn execute(
332        &mut self,
333        taker_order: Order,
334        maker_order: Order,
335        pair_id: impl Into<Vec<u8>>,
336        base_asset_id: impl Into<Vec<u8>>,
337        quote_asset_id: impl Into<Vec<u8>>,
338        now: i64,
339    ) -> Result<(), OrderBookError> {
340        // Normalize IDs up front so we don't move the Into<Vec<u8>> values multiple times
341        let pair_id_vec = pair_id.into();
342        let base_asset_id_vec = base_asset_id.into();
343        let quote_asset_id_vec = quote_asset_id.into();
344        let taker_is_bid = taker_order.is_bid;
345        let (matching_amount, taker_clear, maker_clear) = self._get_matching_amount(taker_order.clone(), maker_order.clone())?;
346        // matching_amount is expressed in taker terms; convert to base/quote by side
347        let matching_base_amount = if taker_is_bid {
348            matching_amount.saturating_mul(1_0000_0000).saturating_div(taker_order.price)
349        } else {
350            matching_amount
351        };
352        let matching_quote_amount = if taker_is_bid {
353            matching_amount
354        } else {
355            matching_amount.saturating_mul(taker_order.price).saturating_div(1_0000_0000)
356        };
357
358        let taker_matching_amount = if taker_is_bid { matching_quote_amount.clone() } else { matching_base_amount.clone() };
359        let maker_matching_amount = if taker_is_bid { matching_base_amount.clone() } else { matching_quote_amount.clone() };
360
361        // Get order data before mutable borrow
362        if maker_order.expires_at <= now {
363            self._expire_order(maker_order.id, !taker_is_bid, pair_id_vec.clone(), now)?;
364            // let _match_at at pair.rs handle the expired order error
365            return Err(OrderBookError::OrderExpired);
366        }
367        let (_taker_delete_amount, taker_delete_price) =
368            self.l3
369                .decrease_order(taker_order.id, taker_matching_amount, self.dust, taker_clear)?;
370        let (_maker_delete_amount, maker_delete_price) =
371            self.l3
372                .decrease_order(maker_order.id, maker_matching_amount, self.dust, maker_clear)?;
373
374        // Calculate fees using fee table
375        let (base_fee, quote_fee) = self._calculate_fees(
376            taker_is_bid,
377            matching_base_amount,
378            matching_quote_amount,
379            maker_order.fee_bps,
380            taker_order.fee_bps,
381        );
382
383        // emit the event for order matched
384        let match_timestamp = now;
385
386        let (taker_remaining_cqty, taker_remaining_pqty) = match self.l3.get_order(taker_order.id) {
387            Ok(updated) => (updated.cqty, updated.pqty),
388            Err(_) => (0, 0),
389        };
390
391        // emit event for both maker and taker
392        let (maker_remaining_cqty, maker_remaining_pqty) = match self.l3.get_order(maker_order.id) {
393            Ok(updated) => (updated.cqty, updated.pqty),
394            Err(_) => (0, 0),
395        };
396        let taker_delta_cqty = taker_order.cqty.saturating_sub(taker_remaining_cqty);
397        let taker_delta_pqty = taker_order.pqty.saturating_sub(taker_remaining_pqty);
398        let maker_delta_cqty = maker_order.cqty.saturating_sub(maker_remaining_cqty);
399        let maker_delta_pqty = maker_order.pqty.saturating_sub(maker_remaining_pqty);
400        // emit taker / maker order history event
401        self._emit_taker_maker_match(
402            taker_order.clone(),
403            maker_order.clone(),
404            taker_remaining_cqty,
405            taker_remaining_pqty,
406            maker_remaining_cqty,
407            maker_remaining_pqty,
408            pair_id_vec.clone(),
409            base_asset_id_vec.clone(),
410            quote_asset_id_vec.clone(),
411            matching_base_amount,
412            matching_quote_amount,
413            base_fee,
414            quote_fee,
415            match_timestamp,
416            taker_order.expires_at,
417            maker_order.expires_at,
418        )?;
419
420        // adjust price level on the matched amount
421        // Update levels and remove price if level becomes 0 or below
422        // Also handle delete_price removal if an order was fully consumed
423        self.update_price_level(
424            pair_id_vec.clone(),
425            false,
426            taker_order.is_bid,
427            taker_order.price,
428            taker_delta_pqty,
429            taker_delta_cqty,
430            taker_delete_price,
431        )?;
432        self.update_price_level(
433            pair_id_vec.clone(),
434            false,
435            maker_order.is_bid,
436            maker_order.price,
437            maker_delta_pqty,
438            maker_delta_cqty,
439            maker_delete_price,
440        )?;
441
442        self._emit_order_rebates(pair_id_vec.clone(), base_asset_id_vec.clone(), quote_asset_id_vec.clone(), maker_order.clone(), taker_order.clone(), matching_base_amount, matching_quote_amount)?;
443
444        Ok(())
445    }
446
447    fn _emit_order_rebates(
448        &self,
449        pair_id: impl Into<Vec<u8>>,
450        base_asset_id: impl Into<Vec<u8>>,
451        quote_asset_id: impl Into<Vec<u8>>,
452        maker_order: Order,
453        taker_order: Order,
454        matching_base_amount: u64,
455        matching_quote_amount: u64,
456    ) -> Result<(), OrderBookError> {
457        let pair_id_vec = pair_id.into();
458        let base_asset_id_vec = base_asset_id.into();
459        let quote_asset_id_vec = quote_asset_id.into();
460
461        // check if maker order is rebatable 
462        if maker_order.fee_bps < 0 {
463            let now = std::time::SystemTime::now()
464                .duration_since(std::time::UNIX_EPOCH)
465                .unwrap()
466                .as_millis() as i64;
467            event::emit_event(SpotEvent::SpotPairRebate {
468                pair_id: pair_id_vec.clone(),
469                base_asset_id: base_asset_id_vec.clone(),
470                quote_asset_id: quote_asset_id_vec.clone(),
471                price: maker_order.price,
472                is_bid: maker_order.is_bid,
473                account_id: maker_order.owner.clone(),
474                is_maker: true,
475                fee_bps: maker_order.fee_bps as i16,
476                base_volume: matching_base_amount,
477                quote_volume: matching_quote_amount,
478                timestamp: now,
479            });
480        }
481        if taker_order.fee_bps < 0 {
482            let now = std::time::SystemTime::now()
483                .duration_since(std::time::UNIX_EPOCH)
484                .unwrap()
485                .as_millis() as i64;
486            event::emit_event(SpotEvent::SpotPairRebate {
487                pair_id: pair_id_vec.clone(),
488                base_asset_id: base_asset_id_vec.clone(),
489                quote_asset_id: quote_asset_id_vec.clone(),
490                price: taker_order.price,
491                is_bid: taker_order.is_bid,
492                account_id: taker_order.owner.clone(),
493                is_maker: false,
494                fee_bps: taker_order.fee_bps as i16,
495                base_volume: matching_base_amount,
496                quote_volume: matching_quote_amount,
497                timestamp: now,
498            });
499        }
500        Ok(())
501    }
502
503    /// Determines the matching amount between the taker and maker orders.
504    /// - `taker_order` is the taker order.
505    /// - `maker_order` is the maker order.
506    /// - returns the matching amount and whether the taker order is fully matched and whether the maker order is fully matched.
507    fn _get_matching_amount(
508        &mut self,
509        taker_order: Order,
510        maker_order: Order,
511    ) -> Result<(u64, bool, bool), OrderBookError> {
512        let taker_converted_matching_cqty = if taker_order.is_bid {
513            taker_order.cqty.saturating_mul(1_0000_0000).saturating_div(taker_order.price)
514        } else {
515            taker_order.cqty.saturating_mul(taker_order.price).saturating_div(1_0000_0000)
516        };
517        // there are two cases:
518        // 1. taker order's converted matching amount is bigger than maker order's matching amount
519        if taker_converted_matching_cqty >= maker_order.cqty {
520            // get the taker's matching amount from the maker order
521            let taker_matching_amount = self.get_required(maker_order.clone(), taker_order.price, maker_order.cqty)?;
522            return Ok((taker_matching_amount, false, true));
523        } 
524        // 2. taker order's converted matching amount is smaller than maker order's matching amount
525        else if taker_converted_matching_cqty < maker_order.cqty {
526            // get the maker's matching amount from the taker order
527            let maker_matching_cqty = self.get_required(taker_order.clone(), maker_order.price, taker_order.cqty)?;
528            return Ok((maker_matching_cqty, true, false));
529        }
530        // 3. taker order's converted matching amount is equal to maker order's matching amount
531        else { 
532            // get the taker's matching amount from the maker order
533            return Ok((taker_order.cqty, true, true)); 
534        }
535    }
536
537    fn _calculate_fees(
538        &self,
539        is_bid: bool,
540        matching_base_amount: u64,
541        matching_quote_amount: u64,
542        mut maker_fee_bps: i16,
543        mut taker_fee_bps: i16,
544    ) -> (u64, u64) {
545        // find maker and taker from base and quote amount
546        // redefine 
547        maker_fee_bps = if maker_fee_bps < 0 { 0 } else { maker_fee_bps };
548        taker_fee_bps = if taker_fee_bps < 0 { 0 } else { taker_fee_bps };
549        if is_bid {
550            (
551                matching_base_amount * maker_fee_bps as u64 / 10000,
552                matching_quote_amount * taker_fee_bps as u64 / 10000,
553            )
554        } else {
555            (
556                matching_base_amount * taker_fee_bps as u64 / 10000,
557                matching_quote_amount * maker_fee_bps as u64 / 10000,
558            )
559        }
560    }
561
562    fn _expire_order(
563        &mut self,
564        order_id: OrderId,
565        is_bid: bool,
566        pair_id: Vec<u8>,
567        now: i64,
568    ) -> Result<(), OrderBookError> {
569        let order = self.l3.get_order(order_id)?.clone();
570        let deleted_price_opt = self.l3.delete_order(order_id)?;
571        // update the price level on the orderbook
572        self.update_price_level(
573            pair_id,
574            false,
575            is_bid,
576            order.price,
577            order.pqty,
578            order.cqty,
579            deleted_price_opt,
580        )?;
581        // emit event for the order expired
582        event::emit_event(SpotEvent::SpotOrderExpired {
583            cid: order.cid.clone(),
584            order_id: order_id.to_bytes().to_vec(),
585            maker_account_id: order.owner.clone(),
586            is_bid,
587            price: order.price,
588            amnt: order.amnt,
589            iqty: order.iqty,
590            pqty: order.pqty,
591            cqty: order.cqty,
592            timestamp: now,
593            expires_at: order.expires_at,
594        });
595        Ok(())
596    }
597
598    fn _emit_taker_maker_match(
599        &self,
600        taker_order: Order,
601        maker_order: Order,
602        taker_remaining_cqty: u64,
603        taker_remaining_pqty: u64,
604        maker_remaining_cqty: u64,
605        maker_remaining_pqty: u64,
606        pair_id: impl Into<Vec<u8>>,
607        base_asset_id: impl Into<Vec<u8>>,
608        quote_asset_id: impl Into<Vec<u8>>,
609        matching_base_amount: u64,
610        matching_quote_amount: u64,
611        base_fee: u64,
612        quote_fee: u64,
613        timestamp: i64,
614        taker_expires_at: i64,
615        maker_expires_at: i64,
616    ) -> Result<(), OrderBookError> {
617        let pair_id_vec = pair_id.into();
618        let base_asset_id_vec = base_asset_id.into();
619        let quote_asset_id_vec = quote_asset_id.into();
620
621        // emit event for taker order filled
622        if taker_remaining_cqty > 0 {
623            event::emit_event(SpotEvent::SpotOrderPartiallyFilled {
624                is_taker_event: true,
625                taker_cid: taker_order.cid.clone(),
626                maker_cid: maker_order.cid.clone(),
627                taker_order_id: taker_order.id.to_bytes().to_vec(),
628                maker_order_id: maker_order.id.to_bytes().to_vec(),
629                taker_account_id: taker_order.owner.clone(),
630                maker_account_id: maker_order.owner.clone(),
631                taker_order_is_bid: taker_order.is_bid,
632                maker_order_is_bid: maker_order.is_bid,
633                price: taker_order.price,
634                pair_id: pair_id_vec.clone(),
635                base_asset_id: base_asset_id_vec.clone(),
636                quote_asset_id: quote_asset_id_vec.clone(),
637                base_volume: matching_base_amount,
638                quote_volume: matching_quote_amount,
639                base_fee: base_fee,
640                quote_fee: quote_fee,
641                maker_fee_bps: maker_order.fee_bps,
642                taker_fee_bps: taker_order.fee_bps,
643                amnt: taker_order.amnt,
644                iqty: taker_order.iqty,
645                pqty: taker_remaining_pqty,
646                cqty: taker_remaining_cqty,
647                timestamp: timestamp,
648                expires_at: taker_expires_at,
649            });
650        } else {
651            event::emit_event(SpotEvent::SpotOrderFullyFilled {
652                is_taker_event: true,
653                taker_cid: taker_order.cid.clone(),
654                maker_cid: maker_order.cid.clone(),
655                taker_order_id: taker_order.id.to_bytes().to_vec(),
656                maker_order_id: maker_order.id.to_bytes().to_vec(),
657                taker_account_id: taker_order.owner.clone(),
658                maker_account_id: maker_order.owner.clone(),
659                taker_order_is_bid: taker_order.is_bid,
660                maker_order_is_bid: maker_order.is_bid,
661                price: taker_order.price,
662                pair_id: pair_id_vec.clone(),
663                base_asset_id: base_asset_id_vec.clone(),
664                quote_asset_id: quote_asset_id_vec.clone(),
665                base_volume: matching_base_amount,
666                quote_volume: matching_quote_amount,
667                base_fee: base_fee,
668                quote_fee: quote_fee,
669                maker_fee_bps: maker_order.fee_bps,
670                taker_fee_bps: taker_order.fee_bps,
671                amnt: taker_order.amnt,
672                iqty: taker_order.iqty,
673                pqty: taker_remaining_pqty,
674                cqty: taker_remaining_cqty,
675                timestamp: timestamp,
676                expires_at: taker_expires_at,
677            });
678        }
679
680        // emit event for maker order filled
681        if maker_remaining_cqty > 0 {
682            event::emit_event(SpotEvent::SpotOrderPartiallyFilled {
683                is_taker_event: false,
684                taker_cid: taker_order.cid.clone(),
685                maker_cid: maker_order.cid.clone(),
686                taker_order_id: taker_order.id.to_bytes().to_vec(),
687                maker_order_id: maker_order.id.to_bytes().to_vec(),
688                taker_account_id: taker_order.owner.clone(),
689                maker_account_id: maker_order.owner.clone(),
690                taker_order_is_bid: taker_order.is_bid,
691                maker_order_is_bid: maker_order.is_bid,
692                pair_id: pair_id_vec.clone(),
693                base_asset_id: base_asset_id_vec.clone(),
694                quote_asset_id: quote_asset_id_vec.clone(),
695                price: maker_order.price,
696                base_volume: matching_base_amount,
697                quote_volume: matching_quote_amount,
698                base_fee: base_fee,
699                quote_fee: quote_fee,
700                maker_fee_bps: maker_order.fee_bps,
701                taker_fee_bps: taker_order.fee_bps,
702                amnt: maker_order.amnt,
703                iqty: maker_order.iqty,
704                pqty: maker_remaining_pqty,
705                cqty: maker_remaining_cqty,
706                timestamp: timestamp,
707                expires_at: maker_expires_at,
708            });
709        } else {
710            event::emit_event(SpotEvent::SpotOrderFullyFilled {
711                is_taker_event: false,
712                taker_cid: taker_order.cid.clone(),
713                maker_cid: maker_order.cid.clone(),
714                taker_order_id: taker_order.id.to_bytes().to_vec(),
715                maker_order_id: maker_order.id.to_bytes().to_vec(),
716                taker_account_id: taker_order.owner.clone(),
717                maker_account_id: maker_order.owner.clone(),
718                taker_order_is_bid: taker_order.is_bid,
719                maker_order_is_bid: maker_order.is_bid,
720                pair_id: pair_id_vec.clone(),
721                base_asset_id: base_asset_id_vec.clone(),
722                quote_asset_id: quote_asset_id_vec.clone(),
723                price: maker_order.price,
724                base_volume: matching_base_amount,
725                quote_volume: matching_quote_amount,
726                base_fee: base_fee,
727                quote_fee: quote_fee,
728                maker_fee_bps: maker_order.fee_bps,
729                taker_fee_bps: taker_order.fee_bps,
730                amnt: maker_order.amnt,
731                iqty: maker_order.iqty,
732                pqty: maker_remaining_pqty,
733                cqty: maker_remaining_cqty,
734                timestamp: timestamp,
735                expires_at: maker_expires_at,
736            });
737        }
738        Ok(())
739    }
740
741    /// updates the levels on the orderbook in the price level linked list
742    /// - `cid` is the client id.
743    /// - `pair_id` is the pair id.
744    /// - `is_placed` is whether the order is placed.
745    /// - `is_bid` is whether the order is a bid order.
746    /// - `price` is the price of the order.
747    /// - `delta_pqty` is the delta quantity of the public quantity.
748    /// - `delta_cqty` is the delta quantity of the current quantity.
749    /// - `delete_price` is an optional price that should be removed (when an order was fully consumed).
750    /// Removes the price if the level becomes 0 or below.
751    pub fn update_price_level(
752        &mut self,
753        pair_id: Vec<u8>,
754        is_placed: bool,
755        is_bid: bool,
756        price: u64,
757        delta_pqty: u64,
758        delta_cqty: u64,
759        delete_price: Option<u64>,
760    ) -> Result<(), OrderBookError> {
761        if is_placed {
762            // insert price if the price does not exist
763            if !self.l2.price_exists(is_bid, price) {
764                self.l2.insert_price(is_bid, price)?;
765                // Initialize level to 0 for new price
766                if is_bid {
767                    self.l2.set_public_bid_level(price, delta_pqty)?;
768                    self.l2.set_current_bid_level(price, delta_cqty)?;
769                } else {
770                    self.l2.set_public_ask_level(price, delta_pqty)?;
771                    self.l2.set_current_ask_level(price, delta_cqty)?;
772                }
773                return Ok(());
774            }
775
776            // throw L2 price missing error if price is not found
777            let current_pqty;
778            let current_cqty;
779            if is_bid {
780                current_pqty = self.l2.public_bid_level(price).ok_or(OrderBookError::L2(
781                    L2Error::PriceMissing {
782                        price,
783                        is_bid,
784                        is_placed,
785                    },
786                ))?;
787                current_cqty = self.l2.current_bid_level(price).ok_or(OrderBookError::L2(
788                    L2Error::PriceMissing {
789                        price,
790                        is_bid,
791                        is_placed,
792                    },
793                ))?;
794            } else {
795                current_pqty = self.l2.public_ask_level(price).ok_or(OrderBookError::L2(
796                    L2Error::PriceMissing {
797                        price,
798                        is_bid,
799                        is_placed,
800                    },
801                ))?;
802                current_cqty = self.l2.current_ask_level(price).ok_or(OrderBookError::L2(
803                    L2Error::PriceMissing {
804                        price,
805                        is_bid,
806                        is_placed,
807                    },
808                ))?;
809            }
810
811            // Calculate new level quantity (add amount)
812            let new_pqty = current_pqty.saturating_add(delta_pqty);
813            let new_cqty = current_cqty.saturating_add(delta_cqty);
814
815            if is_bid {
816                self.l2.set_public_bid_level(price, new_pqty)?;
817                self.l2.set_current_bid_level(price, new_cqty)?;
818            } else {
819                self.l2.set_public_ask_level(price, new_pqty)?;
820                self.l2.set_current_ask_level(price, new_cqty)?;
821            }
822
823            // emit the event for the price level update on the orderbook
824            event::emit_event(SpotEvent::SpotPriceLevelChanged {
825                pair_id,
826                is_bid,
827                price,
828                pqty: new_pqty,
829                cqty: new_cqty,
830                timestamp: std::time::SystemTime::now()
831                    .duration_since(std::time::UNIX_EPOCH)
832                    .unwrap()
833                    .as_millis() as i64,
834            });
835            Ok(())
836        } else {
837            // Get current level quantity
838            let current_cqty;
839            let public_cqty;
840            if is_bid {
841                public_cqty = self.l2.public_bid_level(price).ok_or(OrderBookError::L2(
842                    L2Error::PriceMissing {
843                        price,
844                        is_bid,
845                        is_placed,
846                    },
847                ))?;
848                current_cqty = self.l2.current_bid_level(price).ok_or(OrderBookError::L2(
849                    L2Error::PriceMissing {
850                        price,
851                        is_bid,
852                        is_placed,
853                    },
854                ))?;
855            } else {
856                public_cqty = self.l2.public_ask_level(price).ok_or(OrderBookError::L2(
857                    L2Error::PriceMissing {
858                        price,
859                        is_bid,
860                        is_placed,
861                    },
862                ))?;
863                current_cqty = self.l2.current_ask_level(price).ok_or(OrderBookError::L2(
864                    L2Error::PriceMissing {
865                        price,
866                        is_bid,
867                        is_placed,
868                    },
869                ))?;
870            };
871
872            // Calculate new level quantity (subtract amount)
873            // Use saturating_sub to prevent underflow, but we still check for 0
874            let new_cqty = current_cqty.saturating_sub(delta_cqty);
875            let new_pqty = public_cqty.saturating_sub(delta_pqty);
876
877            // Update the level
878            if is_bid {
879                if new_cqty > 0 {
880                    self.l2.set_public_bid_level(price, new_pqty)?;
881                    self.l2.set_current_bid_level(price, new_cqty)?;
882                } else {
883                    // Level is 0 or below, remove the price
884                    self.l2.set_public_bid_level(price, 0)?;
885                    self.l2.set_current_bid_level(price, 0)?;
886                    // Check if price level is empty in L3, and if so, remove it
887                    if self.l3.is_empty(price) {
888                        self.l2.remove_price(is_bid, price)?;
889                    }
890                }
891            } else {
892                if new_cqty > 0 {
893                    self.l2.set_public_ask_level(price, new_pqty)?;
894                    self.l2.set_current_ask_level(price, new_cqty)?;
895                } else {
896                    // Level is 0 or below, remove the price
897                    self.l2.set_public_ask_level(price, 0)?;
898                    self.l2.set_current_ask_level(price, 0)?;
899                    // Check if price level is empty in L3, and if so, remove it
900                    if self.l3.is_empty(price) {
901                        self.l2.remove_price(is_bid, price)?;
902                    }
903                }
904            }
905
906            // If delete_price is Some, it means an order was fully consumed and price level was emptied
907            // Remove that price level
908            if let Some(delete_price) = delete_price {
909                self.l2.remove_price(is_bid, delete_price)?;
910            }
911
912            // emit the event for the price level update on the orderbook
913            event::emit_event(SpotEvent::SpotPriceLevelChanged {
914                pair_id,
915                is_bid,
916                price,
917                pqty: new_pqty,
918                cqty: new_cqty,
919                timestamp: std::time::SystemTime::now()
920                    .duration_since(std::time::UNIX_EPOCH)
921                    .unwrap()
922                    .as_millis() as i64,
923            });
924
925            Ok(())
926        }
927    }
928
929    /// Cancels an order.
930    /// - returns the amount to send and the delete price.
931    /// - `order_id` is the id of the order to cancel.
932    /// - `owner` is the owner of the order.
933    pub fn cancel_order(
934        &mut self,
935        cid: impl Into<Vec<u8>>,
936        pair_id: impl Into<Vec<u8>>,
937        is_bid: bool,
938        order_id: OrderId,
939        owner: impl Into<Vec<u8>>,
940    ) -> Result<(), OrderBookError> {
941        let cid = cid.into();
942        let pair_id = pair_id.into();
943        let owner = owner.into();
944        // check if the order exists
945        let order = self.l3.get_order(order_id)?.clone();
946        // check if the owner is the same as the owner of the order
947        if order.owner != owner {
948            return Err(OrderBookError::OrderNotOwnedBySender);
949        }
950        let deleted_price_opt = self.l3.delete_order(order_id)?;
951        if let Some(price) = deleted_price_opt {
952            self.l2.remove_price(is_bid, price)?;
953        }
954
955        // emit the event for the order cancelled
956        event::emit_event(SpotEvent::SpotOrderCancelled {
957            cid: cid.clone(),
958            order_id: order_id.to_bytes().to_vec(),
959            maker_account_id: order.owner.clone(),
960            is_bid,
961            price: order.price,
962            amnt: order.amnt,
963            iqty: order.iqty,
964            pqty: order.pqty,
965            cqty: order.cqty,
966            timestamp: order.timestamp,
967            expires_at: order.expires_at,
968        });
969
970        // update the price level on the orderbook
971        self.update_price_level(
972            pair_id.clone(),
973            false,
974            is_bid,
975            order.price,
976            order.pqty,
977            order.cqty,
978            deleted_price_opt,
979        )?;
980        Ok(())
981    }
982
983    pub fn expire_orders(
984        &mut self,
985        is_bid: bool,
986        pair_id: impl Into<Vec<u8>>,
987        base_asset_id: impl Into<Vec<u8>>,
988        quote_asset_id: impl Into<Vec<u8>>,
989        managing_account_id: impl Into<Vec<u8>>,
990        now: i64,
991    ) -> Result<(), OrderBookError> {
992        let pair_id = pair_id.into();
993        let base_asset_id = base_asset_id.into();
994        let quote_asset_id = quote_asset_id.into();
995        let managing_account_id = managing_account_id.into();
996        let expired_orders = self.l3.remove_dormant_orders(now);
997        for (order_id, order) in expired_orders {
998            // emit event for the order expired
999            event::emit_event(SpotEvent::SpotOrderExpired {
1000                cid: order.cid.clone(),
1001                order_id: order_id.to_bytes().to_vec(),
1002                maker_account_id: order.owner.clone(),
1003                is_bid,
1004                price: order.price,
1005                amnt: order.amnt,
1006                iqty: order.iqty,
1007                pqty: order.pqty,
1008                cqty: order.cqty,
1009                timestamp: now,
1010                expires_at: order.expires_at,
1011            });
1012            // emit event for transfer of the expired asset to order owner
1013            let expired_asset_id = if is_bid {
1014                quote_asset_id.clone()
1015            } else {
1016                base_asset_id.clone()
1017            };
1018            event::emit_event(SpotEvent::Transfer {
1019                cid: order.cid.clone(),
1020                from: managing_account_id.clone(),
1021                to: order.owner.clone(),
1022                asset: expired_asset_id,
1023                amnt: order.amnt,
1024                timestamp: now,
1025            });
1026
1027            // update the price level on the orderbook
1028            let delete_price = if self.l3.is_empty(order.price) {
1029                Some(order.price)
1030            } else {
1031                None
1032            };
1033            self.update_price_level(
1034                pair_id.clone(),
1035                false,
1036                is_bid,
1037                order.price,
1038                order.pqty,
1039                order.cqty,
1040                delete_price,
1041            )?;
1042        }
1043        Ok(())
1044    }
1045
1046    pub fn set_iceberg_quantity(
1047        &mut self,
1048        cid: impl Into<Vec<u8>>,
1049        pair_id: impl Into<Vec<u8>>,
1050        is_bid: bool,
1051        order_id: OrderId,
1052        iqty: u64,
1053    ) -> Result<(), OrderBookError> {
1054        let cid = cid.into();
1055        let pair_id = pair_id.into();
1056        let before = self.l3.get_order(order_id)?.clone();
1057        let order = self.l3.set_iceberg_quantity(order_id, iqty)?;
1058        let (is_placed, delta_pqty) = if order.pqty >= before.pqty {
1059            (true, order.pqty - before.pqty)
1060        } else {
1061            (false, before.pqty - order.pqty)
1062        };
1063        if delta_pqty > 0 {
1064            self.update_price_level(
1065                pair_id,
1066                is_placed,
1067                is_bid,
1068                order.price,
1069                delta_pqty,
1070                0,
1071                None,
1072            )?;
1073        }
1074        // emit event for the iceberg quantity changed
1075        event::emit_event(SpotEvent::SpotOrderIcebergQuantityChanged {
1076            cid,
1077            order_id: order_id.to_bytes().to_vec(),
1078            amnt: order.amnt,
1079            iqty: iqty,
1080            pqty: order.pqty,
1081            cqty: order.cqty,
1082            timestamp: order.timestamp,
1083            expires_at: order.expires_at,
1084        });
1085        Ok(())
1086    }
1087}