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