Skip to main content

clob_engine/order_book/
matching_engine.rs

1use crate::order_book::{
2    orderbook::OrderBook, types::{
3        BookDepth, CancelOutcome, EngineCancelOrder, EngineModifyOrder, EngineNewOrder, MatchOutcome, ModifyOutcome, OrderNode, OrderType
4    }
5};
6use anyhow::{Context, anyhow};
7use std::collections::HashMap;
8use tracing::{Span};
9
10#[derive(Debug)]
11pub struct MatchingEngine {
12    _book: HashMap<u32, OrderBook>
13}
14
15impl MatchingEngine {
16
17    pub fn new() -> Self{
18        Self { _book: HashMap::new()}
19    }
20
21    fn get_orderbook(
22        &mut self,
23        security_id : u32
24    ) -> Option<&mut OrderBook> {
25        
26        let Some(book) = self._book.get_mut(&security_id) else {
27            return None;
28        };
29        Some(book)
30    }
31
32    pub fn modify(
33        &mut self,
34        order_id: u64,
35        security_id : u32,
36        new_price: Option<u32>,
37        new_qty: Option<u32>,
38        is_buy_side : bool,
39        span: &Span,
40    ) -> Result< &'static str, anyhow::Error> {
41        let _gaurd = span.enter();
42        let orderbook = self
43            .get_orderbook(security_id)
44            .context("Could not find the orderbook")?;
45        if let Ok(potential_modfication) = orderbook.modify_order(
46            order_id,
47            EngineModifyOrder {
48                order_id,
49                security_id,
50                new_price,
51                is_buy_side,
52                new_quantity: new_qty,
53            },
54        ) {
55            if let Some(modification_result) = potential_modfication {
56                match modification_result {
57                    ModifyOutcome::Both {
58                        new_price,
59                        new_initial_qty,
60                        old_current_qty,
61                    } => {
62                        span.record("modify_outcome", "price & qty");
63                        let _ = self.match_order(
64                            EngineNewOrder {
65                                engine_order_id: order_id,
66                                price: Some(new_price),
67                                initial_quantity: new_initial_qty,
68                                current_quantity : old_current_qty,
69                                is_buy_side,
70                                security_id,
71                                order_type: OrderType::Limit,
72                            },
73                        span);
74                        return Ok("Both")
75                    },
76                    ModifyOutcome::Repriced { new_price, old_initial_qty, old_current_qty } => 
77                        {
78                        span.record("modify_outcome", "price");
79                            let _ = self.match_order(
80                            EngineNewOrder {
81                                engine_order_id: order_id,
82                                price: Some(new_price),
83                                initial_quantity: old_initial_qty,
84                                current_quantity : old_current_qty,
85                                is_buy_side,
86                                security_id,
87                                order_type: OrderType::Limit,
88                            },
89                        span);
90                        return Ok("Repriced")
91                    },
92                    ModifyOutcome::Requantized { old_price, new_initial_qty, old_current_qty } => {
93                            let _ = self.match_order(
94                            EngineNewOrder {
95                                engine_order_id: order_id,
96                                price: Some(old_price),
97                                initial_quantity: new_initial_qty,
98                                current_quantity : old_current_qty,
99                                is_buy_side,
100                                security_id,
101                                order_type: OrderType::Limit,
102                            }, span);
103                            return Ok("Requantized")
104                    },
105                    ModifyOutcome::Inplace => {
106                        span.record("modify_outcome", "qty reduction");
107                        return Ok("Inplace")
108                    }
109                }
110            }
111            return Ok("No potential modification")
112        } else {
113            return Ok("No modification occured");
114        }
115    }
116
117    pub fn cancel(&mut self, order_id: u64,security_id : u32, span: &Span, is_buy_side : bool) -> Result<CancelOutcome, anyhow::Error>{
118        let orderbook = self
119            .get_orderbook(security_id)
120            .context("Could not find the orderbook")?;
121        if let Err(_) = orderbook.cancel_order(order_id, EngineCancelOrder{is_buy_side,security_id, order_id}){
122            span.record("reason", "orderbook cancellation failed");
123            span.record("success_status", false);
124            return Ok(CancelOutcome::Failed);
125        }; 
126        span.record("success_status", true);
127        return Ok(CancelOutcome::Success);
128    }
129
130    pub fn depth(&self, security_id : u32, levels_count :Option<u32>, span: &Span ) -> Result<BookDepth, anyhow::Error>{
131        let _gaurd = span.enter();
132        span.record("security_id", security_id.to_string());
133        let Some(order_book) = self._book.get(&security_id) else {
134            span.record("status", "failed");
135            span.record("reason", "orderbook doesn't exist");
136            return Err(anyhow!(""))
137        };
138        match order_book.depth(levels_count){
139            Ok(book_depth) => {
140                span.record("status", "success");
141                span.record("reason", "None");
142                Ok(book_depth)
143            },
144            Err(e) => Err(anyhow!("{}", e))
145        }
146    }
147
148    pub fn match_order(&mut self, order: EngineNewOrder, span: &Span) -> Result<MatchOutcome, anyhow::Error> {
149        
150        let _gaurd = span.enter();
151
152        let orderbook = match self._book.get_mut(&order.security_id){
153            Some(orderbook) => {
154                orderbook
155            }
156            None => {
157                self._book.entry(order.security_id).or_insert(OrderBook::new())
158            }
159        };
160
161        if !order.is_buy_side {
162            // for ASK order
163            match order.order_type {
164                OrderType::Market(None) => {
165                    // need to immediatly execute the order on the best of other half
166                    let mut fill_quantity = order.initial_quantity;
167                    let mut levels_consumed = 0;
168                    let mut orders_touched = 0;
169                    while fill_quantity > 0 {
170                        let remove_node: bool;
171                        {
172                            let Some(mut price_node) = orderbook.bid.price_map.last_entry() else {
173                                break;
174                            };
175                            let price_level = price_node.get_mut();
176                            while price_level.total_quantity > 0 && fill_quantity > 0 {
177                                if let Some(head_idx) = price_level.head{
178
179                                    match orderbook.bid.order_pool[head_idx].as_mut(){
180                                        Some(first_order_node) => {
181
182                                            if fill_quantity >= first_order_node.current_quantity {
183                                                fill_quantity -= first_order_node.current_quantity;
184                                                
185                                                price_level.total_quantity = price_level.total_quantity.checked_sub(first_order_node.current_quantity).ok_or(anyhow!("error occured in sub of total qty - current qyt"))?;
186                                                let next = first_order_node.next;
187                                                orderbook.bid.order_pool[head_idx] = None;
188                                                orderbook.bid.free_list.push(head_idx);
189                                                orders_touched += 1;
190                                                if let Some(next_order_idx) = next {
191                                                    price_level.head = Some(next_order_idx);
192                                                } else {
193                                                    span.record("reason", "exhausted");
194                                                    price_level.total_quantity = 0;
195                                                    price_level.head = None;
196                                                    price_level.tail = None;
197                                                    price_level.order_count = 0;
198                                                    break;
199                                                }
200                                            } else {
201                                                first_order_node.current_quantity = first_order_node.current_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fnq - fq"))?;
202                                                price_level.total_quantity = price_level.total_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fntq - fq"))?;
203                                                fill_quantity = 0;
204                                                orders_touched += 1;
205                                                span.record("filled", true);
206                                            }
207                                        }
208                                        None => {
209                                            return Err(anyhow!("failed to get head_idx from order pool"));
210                                        }
211                                    };
212                                }else {
213                                    // price level has no head. i.e head = None
214                                    break;
215                                }
216                            }
217                            remove_node = price_level.total_quantity == 0;
218                        }
219                        if remove_node {
220                            match orderbook.bid.price_map.pop_last(){
221                                Some(_) => {
222                                    levels_consumed += 1;
223                                }
224                                None => {
225                                    break;
226                                }
227                            };
228                        }
229                    }
230                    span.record("order_type", "market");
231                    span.record("is_buy_side", false);
232                    span.record("levels_consumed", levels_consumed);
233                    span.record("orders_touched", orders_touched);
234                    Ok(MatchOutcome{
235                        order_index : None,
236                        levels_consumed,
237                        orders_touched
238                    })
239                }
240                OrderType::Market(market_limit) => {
241                    let mut fill_quantity = order.initial_quantity;
242                    let mut levels_consumed = 0;
243                    let mut orders_touched = 0;
244                    while fill_quantity > 0 {
245                        let remove_node: bool;
246                        {
247                            let Some(mut price_node) = orderbook.bid.price_map.last_entry() else {
248                                break;
249                            };
250
251                            match market_limit {
252                                Some(price) => {
253                                    if price > *price_node.key(){
254                                        break;
255                                    }
256                                }
257                                None => {
258                                    return Err(anyhow!("did not recieve price for market-limit(SELL)"))
259                                }
260                            }
261                            let price_level = price_node.get_mut();
262                            while price_level.total_quantity > 0 && fill_quantity > 0 {
263                                if let Some(head_idx) = price_level.head{
264
265                                    match orderbook.bid.order_pool[head_idx].as_mut(){
266                                        Some(first_order_node) => {
267
268                                            if fill_quantity >= first_order_node.current_quantity {
269                                                fill_quantity -= first_order_node.current_quantity;
270                                                price_level.total_quantity = price_level.total_quantity.checked_sub(first_order_node.current_quantity).ok_or(anyhow!("error occured in sub of total qty - current qyt"))?;
271                                                let next = first_order_node.next;
272                                                orderbook.bid.order_pool[head_idx] = None;
273                                                orderbook.bid.free_list.push(head_idx);
274                                                orders_touched += 1;
275                                                if let Some(next_order_idx) = next {
276                                                    price_level.head = Some(next_order_idx);
277                                                } else {
278                                                    span.record("reason", "exhausted");
279                                                    price_level.total_quantity = 0;
280                                                    price_level.head = None;
281                                                    price_level.tail = None;
282                                                    price_level.order_count = 0;
283                                                    break;
284                                                }
285                                            } else {
286                                                first_order_node.current_quantity = first_order_node.current_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fnq - fq"))?;
287                                                price_level.total_quantity = price_level.total_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fntq - fq"))?;
288                                                fill_quantity = 0;
289                                                orders_touched += 1;
290                                                span.record("filled", true);
291                                            }
292                                        }
293                                        None => {
294                                            return Err(anyhow!("failed to get head_idx from order pool"));
295                                        }
296                                    };
297                                }else {
298                                    break;
299                                }
300                            }
301                            remove_node = price_level.total_quantity == 0;
302                        }
303                        if remove_node {
304                            match orderbook.bid.price_map.pop_last(){
305                                Some(_) => {
306                                    levels_consumed += 1;
307                                }
308                                None => {
309                                    break;
310                                }
311                            };
312                        }
313                    }
314                    span.record("order_type", "market");
315                    span.record("is_buy_side", false);
316                    span.record("levels_consumed", levels_consumed);
317                    span.record("orders_touched", orders_touched);
318                    Ok(MatchOutcome{
319                        order_index : None,
320                        levels_consumed,
321                        orders_touched
322                    })
323                }
324                OrderType::Limit => {
325                    let mut fill_quantity = order.initial_quantity;
326                    let mut levels_consumed = 0;
327                    let mut orders_touched = 0;
328                    while fill_quantity > 0 {
329                        let remove_node: bool;
330                        {
331                            let Some(mut price_node) = orderbook.bid.price_map.last_entry() else {
332                                break;
333                            };
334
335                            match order.price {
336                                Some(price) => {
337                                    if price > *price_node.key(){
338                                        break;
339                                    }
340                                }
341                                None => {
342                                    return Err(anyhow!("did not recieve price for limit order (SELL)"))
343                                }
344                            }
345                            let price_level = price_node.get_mut();
346                            while price_level.total_quantity > 0 && fill_quantity > 0 {
347                                if let Some(head_idx) = price_level.head{
348                                    match orderbook.bid.order_pool[head_idx].as_mut(){
349                                        Some(first_order_node) => {
350                                            if fill_quantity >= first_order_node.current_quantity {
351                                        fill_quantity -= first_order_node.current_quantity;
352                                        price_level.total_quantity = price_level.total_quantity.checked_sub(first_order_node.current_quantity).ok_or(anyhow!("error occured in sub of total qty - current qyt"))?;
353                                        let next = first_order_node.next;
354                                        orderbook.bid.order_pool[head_idx] = None;
355                                        orderbook.bid.free_list.push(head_idx);
356                                        orders_touched += 1;
357                                        if let Some(next_order_idx) = next {
358                                            price_level.head = Some(next_order_idx);
359                                        } else {
360                                            span.record("reason", "partially_filled");
361                                            price_level.total_quantity = 0;
362                                            price_level.head = None;
363                                            price_level.tail = None;
364                                            price_level.order_count = 0;
365                                            break;
366                                        }
367                                    } else {
368                                        first_order_node.current_quantity = first_order_node.current_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fnq - fq"))?;
369                                        price_level.total_quantity = price_level.total_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fntq - fq"))?;
370                                        fill_quantity = 0;
371                                        orders_touched += 1;
372                                        span.record("filled", true);
373                                    }
374                                        }
375                                        None => {
376                                            return Err(anyhow!("failed to get head_idx from order pool"));
377                                        }
378                                    };
379                                }else {
380                                    break;
381                                }
382                            }
383                            remove_node = price_level.total_quantity == 0;
384                        }
385                        if remove_node {
386                            match orderbook.bid.price_map.pop_last(){
387                                Some(_) => {
388                                    levels_consumed += 1;
389                                }
390                                None => {
391                                    break;
392                                }
393                            };
394                        }
395                    }
396                    if fill_quantity > 0 {
397                        let alloted_index = orderbook.create_sell_order(
398                            order.engine_order_id,
399                            OrderNode {
400                                order_id : order.engine_order_id,
401                                initial_quantity: order.initial_quantity,
402                                current_quantity: fill_quantity,
403                                market_limit: order.price.unwrap(),
404                                next: None,
405                                prev: None,
406                            },
407                        )?;
408                        span.record("order_type", "limit");
409                        span.record("is_buy_side", false);
410                        span.record("levels_consumed", levels_consumed);
411                        span.record("orders_touched", orders_touched);
412                        return Ok(MatchOutcome{
413                        order_index : Some(alloted_index as u32),
414                        levels_consumed,
415                        orders_touched
416                    })
417                    }
418                    Ok(MatchOutcome{
419                        order_index : None,
420                        levels_consumed,
421                        orders_touched
422                    })
423                }
424            }
425        } else {
426            match order.order_type {
427                OrderType::Market(None) => {
428                    // need to immediatly execute the order on the best of other half
429                    let mut fill_quantity = order.initial_quantity;
430                    let mut levels_consumed = 0;
431                    let mut orders_touched = 0;
432                    while fill_quantity > 0 {
433                        let remove_node: bool;
434                        {
435                            let Some(mut price_node) = orderbook.ask.price_map.first_entry() else {
436                                break;
437                            };
438                            let price_level = price_node.get_mut();
439                            while price_level.total_quantity > 0 && fill_quantity > 0 {
440                                if let Some(head_idx) = price_level.head{
441                                    match orderbook.ask.order_pool[head_idx].as_mut(){
442                                        Some(first_order_node) => {
443
444                                            if fill_quantity >= first_order_node.current_quantity {
445                                                fill_quantity -= first_order_node.current_quantity;
446                                                price_level.total_quantity = price_level.total_quantity.checked_sub(first_order_node.current_quantity).ok_or(anyhow!("error occured in sub of total qty - current qyt"))?;
447                                                let next = first_order_node.next;
448                                                orderbook.ask.order_pool[head_idx] = None;
449                                                orderbook.ask.free_list.push(head_idx);
450                                                orders_touched += 1;
451                                                if let Some(next_order_idx) = next {
452                                                    price_level.head = Some(next_order_idx);
453                                                } else {
454                                                    span.record("reason", "exhausted");
455                                                    price_level.total_quantity = 0;
456                                                    price_level.head = None;
457                                                    price_level.tail = None;
458                                                    price_level.order_count = 0;
459                                                    break;
460                                                }
461                                            } else {
462                                                first_order_node.current_quantity = first_order_node.current_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fnq - fq"))?;
463                                                price_level.total_quantity = price_level.total_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fntq - fq"))?;
464                                                fill_quantity = 0;
465                                                orders_touched += 1;
466                                                span.record("filled", true);
467                                            }
468                                        }
469                                        None => {
470                                            return Err(anyhow!("failed to get head_idx from order pool"));
471                                        }
472                                    };
473                                }
474                                else {
475                                    break;
476                                }
477                            }
478                            remove_node = price_level.total_quantity == 0;
479                        }
480                        if remove_node {
481                            match orderbook.ask.price_map.pop_first(){
482                                Some(_) => {
483                                    levels_consumed += 1;
484                                }
485                                None => {
486                                    break;
487                                }
488                            };
489                        }
490                    }
491                    span.record("order_type", "market");
492                    span.record("is_buy_side", true);
493                    span.record("levels_consumed", levels_consumed);
494                    span.record("orders_touched", orders_touched);
495                    Ok(MatchOutcome{
496                        order_index : None,
497                        levels_consumed,
498                        orders_touched
499                    })
500                }
501                OrderType::Market(market_limit) => {
502                    let mut fill_quantity = order.initial_quantity;
503                    let mut levels_consumed = 0;
504                    let mut orders_touched = 0;
505                    while fill_quantity > 0 {
506                        let remove_node: bool;
507                        {
508                            let Some(mut price_node) = orderbook.ask.price_map.first_entry() else {
509                                break;
510                            };
511
512                            match market_limit {
513                                Some(price) => {
514                                    if price < *price_node.key(){
515                                        break;
516                                    }
517                                }
518                                None => {
519                                    return Err(anyhow!("did not recieve price for market-limit(BUY)"))
520                                }
521                            }
522                            let price_level = price_node.get_mut();
523                            while price_level.total_quantity > 0 && fill_quantity > 0 {
524                                let head_pointer = price_level.head;
525                                if let Some(head_idx) = head_pointer{
526                                    match orderbook.ask.order_pool[head_idx].as_mut(){
527                                        Some(first_order_node) => {
528
529                                            if fill_quantity >= first_order_node.current_quantity {
530                                                fill_quantity -= first_order_node.current_quantity;
531                                                price_level.total_quantity = price_level.total_quantity.checked_sub(first_order_node.current_quantity).ok_or(anyhow!("error occured in sub of total qty - current qyt"))?;
532                                                let next = first_order_node.next;
533                                                orderbook.ask.order_pool[head_idx] = None;
534                                                orderbook.ask.free_list.push(head_idx);
535                                                orders_touched += 1;
536                                                if let Some(next_order_idx) = next {
537                                                    price_level.head = Some(next_order_idx);
538                                                } else {
539                                                    span.record("reason", "exhausted");
540                                                    price_level.head = None;
541                                                    price_level.total_quantity = 0;
542                                                    price_level.head = None;
543                                                    price_level.tail = None;
544                                                    price_level.order_count = 0;
545                                                    break;
546                                                }
547                                            } else {
548                                                first_order_node.current_quantity = first_order_node.current_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fnq - fq"))?;
549                                                price_level.total_quantity = price_level.total_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fntq - fq"))?;
550                                                fill_quantity = 0;
551                                                orders_touched += 1;
552                                                span.record("filled", true);
553                                            }
554                                        }
555                                        None => {
556                                            return Err(anyhow!("failed to get head_idx from order pool"));
557                                        }
558                                    };
559                                }
560                                else {
561                                    break;
562                                }
563                            }
564                            remove_node = price_level.total_quantity == 0;
565                        }
566                        if remove_node {
567                            match orderbook.ask.price_map.pop_first(){
568                                Some(_) => {
569                                    levels_consumed += 1;
570                                }
571                                None => {
572                                    break;
573                                }
574                            };
575                        }
576                    }
577                    span.record("order_type", "market");
578                    span.record("is_buy_side", true);
579                    span.record("levels_consumed", levels_consumed);
580                    span.record("orders_touched", orders_touched);
581                    Ok(MatchOutcome{
582                        order_index : None,
583                        levels_consumed,
584                        orders_touched
585                    })
586                }
587                OrderType::Limit => {
588                    let mut fill_quantity = order.initial_quantity;
589                    let mut levels_consumed = 0;
590                    let mut orders_touched = 0;
591                    while fill_quantity > 0 {
592                        let remove_node: bool;
593                        {
594                            let Some(mut price_node) = orderbook.ask.price_map.first_entry() else {
595                                break;
596                            };
597
598                            match order.price {
599                                Some(price) => {
600                                    if price < *price_node.key(){
601                                        break;
602                                    }
603                                }
604                                None => {
605                                    return Err(anyhow!("did not recieve price for limit(BUY)"))
606                                }
607                            }
608                            let price_level = price_node.get_mut();
609                            while price_level.total_quantity > 0 && fill_quantity > 0 {
610                                if let Some(head_idx) = price_level.head{
611
612                                    match orderbook.ask.order_pool[head_idx].as_mut(){
613                                        Some(first_order_node) => {
614
615                                            if fill_quantity >= first_order_node.current_quantity {
616                                                fill_quantity -= first_order_node.current_quantity;
617                                                price_level.total_quantity = price_level.total_quantity.checked_sub(first_order_node.current_quantity).ok_or(anyhow!("error occured in sub of total qty - current qyt"))?;
618                                                let next = first_order_node.next;
619                                                orderbook.ask.order_pool[head_idx] = None;
620                                                orderbook.ask.free_list.push(head_idx);
621                                                orders_touched += 1;
622                                                if let Some(next_order_idx) = next {
623                                                    price_level.head = Some(next_order_idx);
624                                                } else {
625                                                    span.record("reason", "partially_filled");
626                                                    price_level.total_quantity = 0;
627                                                    price_level.head = None;
628                                                    price_level.tail = None;
629                                                    price_level.order_count = 0;
630                                                    break;
631                                                }
632                                            } else {
633                                                first_order_node.current_quantity = first_order_node.current_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fnq - fq"))?;
634                                                price_level.total_quantity = price_level.total_quantity.checked_sub(fill_quantity).ok_or(anyhow!("error occured subtracting fntq - fq"))?;
635                                                fill_quantity = 0;
636                                                orders_touched += 1;
637                                                span.record("filled", true);
638                                            }
639                                        }
640                                        None => {
641                                            return Err(anyhow!("failed to get head_idx from order pool"));
642                                        }
643                                    };
644                                }else {
645                                    break;
646                                }
647                            }
648                            remove_node = price_level.total_quantity == 0;
649                        }
650                        if remove_node {
651                            match orderbook.ask.price_map.pop_first(){
652                                Some(_) => {
653                                    levels_consumed += 1;
654                                }
655                                None => {
656                                    break;
657                                }
658                            };
659                        }
660                    }
661                    if fill_quantity > 0{
662                        let alloted_index = orderbook.create_buy_order(
663                            order.engine_order_id,
664                            OrderNode {
665                                order_id : order.engine_order_id,
666                                initial_quantity: order.initial_quantity,
667                                current_quantity: fill_quantity,
668                                market_limit: order.price.unwrap(),
669                                next: None,
670                                prev: None,
671                            },
672                        )?;
673                        span.record("order_type", "limit");
674                        span.record("is_buy_side", true);
675                        span.record("levels_consumed", levels_consumed);
676                        span.record("orders_touched", orders_touched);
677                        return Ok(MatchOutcome{
678                        order_index : Some(alloted_index as u32),
679                        levels_consumed,
680                        orders_touched
681                    })
682                    }
683                    Ok(MatchOutcome{
684                        order_index : None,
685                        levels_consumed,
686                        orders_touched
687                    })
688                }
689            }
690        }
691    }
692}