1use super::Market;
2use super::MarketEvent;
3use super::OrderId;
4use super::RestingOrder;
5use super::WritableMarket;
6use crate::quantities::AdjustedQuoteLots;
7use crate::quantities::BaseLots;
8use crate::quantities::BaseLotsPerBaseUnit;
9use crate::quantities::QuoteLots;
10use crate::quantities::QuoteLotsPerBaseUnit;
11use crate::quantities::QuoteLotsPerBaseUnitPerTick;
12use crate::quantities::Ticks;
13use crate::quantities::WrapperU64;
14use crate::state::inflight_order::InflightOrder;
15use crate::state::matching_engine_response::MatchingEngineResponse;
16use crate::state::*;
17use borsh::{BorshDeserialize, BorshSerialize};
18use bytemuck::{Pod, Zeroable};
19use phoenix_log;
20use sokoban::node_allocator::{NodeAllocatorMap, OrderedNodeAllocatorMap, ZeroCopy, SENTINEL};
21use sokoban::{FromSlice, RedBlackTree};
22use std::fmt::Debug;
23
24#[repr(C)]
25#[derive(
26 Eq, BorshDeserialize, BorshSerialize, PartialEq, Debug, Default, Copy, Clone, Zeroable, Pod,
27)]
28pub struct FIFOOrderId {
29 pub price_in_ticks: Ticks,
35
36 pub order_sequence_number: u64,
45}
46
47impl OrderId for FIFOOrderId {
48 fn price_in_ticks(&self) -> u64 {
49 self.price_in_ticks.as_u64()
50 }
51}
52
53impl FIFOOrderId {
54 pub fn new_from_untyped(price_in_ticks: u64, order_sequence_number: u64) -> Self {
55 FIFOOrderId {
56 price_in_ticks: Ticks::new(price_in_ticks),
57 order_sequence_number,
58 }
59 }
60
61 pub fn new(price_in_ticks: Ticks, order_sequence_number: u64) -> Self {
62 FIFOOrderId {
63 price_in_ticks,
64 order_sequence_number,
65 }
66 }
67}
68
69impl PartialOrd for FIFOOrderId {
70 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
71 let (tick_cmp, seq_cmp) = match Side::from_order_sequence_number(self.order_sequence_number)
76 {
77 Side::Bid => (
78 other.price_in_ticks.partial_cmp(&self.price_in_ticks)?,
79 other
80 .order_sequence_number
81 .partial_cmp(&self.order_sequence_number)?,
82 ),
83 Side::Ask => (
84 self.price_in_ticks.partial_cmp(&other.price_in_ticks)?,
85 self.order_sequence_number
86 .partial_cmp(&other.order_sequence_number)?,
87 ),
88 };
89 if tick_cmp == std::cmp::Ordering::Equal {
90 Some(seq_cmp)
91 } else {
92 Some(tick_cmp)
93 }
94 }
95}
96
97impl Ord for FIFOOrderId {
98 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
99 self.partial_cmp(other).unwrap()
100 }
101}
102
103#[repr(C)]
104#[derive(Default, Debug, Copy, Clone, Zeroable, Pod)]
105pub struct FIFORestingOrder {
106 pub trader_index: u64,
107 pub num_base_lots: BaseLots, pub last_valid_slot: u64,
109 pub last_valid_unix_timestamp_in_seconds: u64,
110}
111
112impl FIFORestingOrder {
113 pub fn new_default(trader_index: u64, num_base_lots: BaseLots) -> Self {
114 FIFORestingOrder {
115 trader_index,
116 num_base_lots,
117 last_valid_slot: 0,
118 last_valid_unix_timestamp_in_seconds: 0,
119 }
120 }
121
122 pub fn new(
123 trader_index: u64,
124 num_base_lots: BaseLots,
125 last_valid_slot: Option<u64>,
126 last_valid_unix_timestamp_in_seconds: Option<u64>,
127 ) -> Self {
128 FIFORestingOrder {
129 trader_index,
130 num_base_lots,
131 last_valid_slot: last_valid_slot.unwrap_or(0),
132 last_valid_unix_timestamp_in_seconds: last_valid_unix_timestamp_in_seconds.unwrap_or(0),
133 }
134 }
135
136 pub fn new_with_last_valid_slot(
137 trader_index: u64,
138 num_base_lots: BaseLots,
139 last_valid_slot: u64,
140 ) -> Self {
141 FIFORestingOrder {
142 trader_index,
143 num_base_lots,
144 last_valid_slot,
145 last_valid_unix_timestamp_in_seconds: 0,
146 }
147 }
148
149 pub fn new_with_last_valid_unix_timestamp(
150 trader_index: u64,
151 num_base_lots: BaseLots,
152 last_valid_unix_timestamp_in_seconds: u64,
153 ) -> Self {
154 FIFORestingOrder {
155 trader_index,
156 num_base_lots,
157 last_valid_slot: 0,
158 last_valid_unix_timestamp_in_seconds,
159 }
160 }
161}
162
163impl RestingOrder for FIFORestingOrder {
164 fn size(&self) -> u64 {
165 self.num_base_lots.as_u64()
166 }
167
168 fn last_valid_slot(&self) -> Option<u64> {
169 if self.last_valid_slot == 0 {
170 None
171 } else {
172 Some(self.last_valid_slot)
173 }
174 }
175
176 fn last_valid_unix_timestamp_in_seconds(&self) -> Option<u64> {
177 if self.last_valid_unix_timestamp_in_seconds == 0 {
178 None
179 } else {
180 Some(self.last_valid_unix_timestamp_in_seconds)
181 }
182 }
183
184 fn is_expired(&self, current_slot: u64, current_unix_timestamp_in_seconds: u64) -> bool {
185 (self.last_valid_slot != 0 && self.last_valid_slot < current_slot)
186 || (self.last_valid_unix_timestamp_in_seconds != 0
187 && self.last_valid_unix_timestamp_in_seconds < current_unix_timestamp_in_seconds)
188 }
189}
190
191#[repr(C)]
192#[derive(Default, Copy, Clone, Zeroable)]
193pub struct FIFOMarket<
194 MarketTraderId: Debug
195 + PartialOrd
196 + Ord
197 + Default
198 + Copy
199 + Clone
200 + Zeroable
201 + Pod
202 + BorshDeserialize
203 + BorshSerialize,
204 const BIDS_SIZE: usize,
205 const ASKS_SIZE: usize,
206 const NUM_SEATS: usize,
207> {
208 pub _padding: [u64; 32],
210
211 pub base_lots_per_base_unit: BaseLotsPerBaseUnit,
213
214 pub tick_size_in_quote_lots_per_base_unit: QuoteLotsPerBaseUnitPerTick,
216
217 order_sequence_number: u64,
219
220 pub taker_fee_bps: u64,
222
223 collected_quote_lot_fees: QuoteLots,
225
226 unclaimed_quote_lot_fees: QuoteLots,
228
229 pub bids: RedBlackTree<FIFOOrderId, FIFORestingOrder, BIDS_SIZE>,
231
232 pub asks: RedBlackTree<FIFOOrderId, FIFORestingOrder, ASKS_SIZE>,
234
235 pub traders: RedBlackTree<MarketTraderId, TraderState, NUM_SEATS>,
237}
238
239unsafe impl<
240 MarketTraderId: Debug
241 + PartialOrd
242 + Ord
243 + Default
244 + Copy
245 + Clone
246 + Zeroable
247 + Pod
248 + BorshDeserialize
249 + BorshSerialize,
250 const BIDS_SIZE: usize,
251 const ASKS_SIZE: usize,
252 const NUM_SEATS: usize,
253 > Pod for FIFOMarket<MarketTraderId, BIDS_SIZE, ASKS_SIZE, NUM_SEATS>
254{
255}
256
257impl<
258 MarketTraderId: Debug
259 + PartialOrd
260 + Ord
261 + Default
262 + Copy
263 + Clone
264 + Zeroable
265 + Pod
266 + BorshDeserialize
267 + BorshSerialize,
268 const BIDS_SIZE: usize,
269 const ASKS_SIZE: usize,
270 const NUM_SEATS: usize,
271 > FromSlice for FIFOMarket<MarketTraderId, BIDS_SIZE, ASKS_SIZE, NUM_SEATS>
272{
273 fn new_from_slice(data: &mut [u8]) -> &mut Self {
274 let market = Self::load_mut_bytes(data).unwrap();
275 assert_eq!(market.base_lots_per_base_unit, BaseLotsPerBaseUnit::ZERO);
276 assert_eq!(market.order_sequence_number, 0);
277 market.initialize();
278 market
279 }
280}
281
282impl<
283 MarketTraderId: Debug
284 + PartialOrd
285 + Ord
286 + Default
287 + Copy
288 + Clone
289 + Zeroable
290 + Pod
291 + BorshDeserialize
292 + BorshSerialize,
293 const BIDS_SIZE: usize,
294 const ASKS_SIZE: usize,
295 const NUM_SEATS: usize,
296 > ZeroCopy for FIFOMarket<MarketTraderId, BIDS_SIZE, ASKS_SIZE, NUM_SEATS>
297{
298}
299
300impl<
301 MarketTraderId: Debug
302 + PartialOrd
303 + Ord
304 + Default
305 + Copy
306 + Clone
307 + Zeroable
308 + Pod
309 + BorshDeserialize
310 + BorshSerialize,
311 const BIDS_SIZE: usize,
312 const ASKS_SIZE: usize,
313 const NUM_SEATS: usize,
314 > Market<MarketTraderId, FIFOOrderId, FIFORestingOrder, OrderPacket>
315 for FIFOMarket<MarketTraderId, BIDS_SIZE, ASKS_SIZE, NUM_SEATS>
316{
317 fn get_data_size(&self) -> usize {
318 std::mem::size_of::<Self>()
319 }
320
321 fn get_taker_fee_bps(&self) -> u64 {
322 self.taker_fee_bps
323 }
324
325 fn get_tick_size(&self) -> QuoteLotsPerBaseUnitPerTick {
326 self.tick_size_in_quote_lots_per_base_unit
327 }
328
329 fn get_base_lots_per_base_unit(&self) -> BaseLotsPerBaseUnit {
330 self.base_lots_per_base_unit
331 }
332
333 fn get_sequence_number(&self) -> u64 {
334 self.order_sequence_number
335 }
336
337 fn get_collected_fee_amount(&self) -> QuoteLots {
338 self.collected_quote_lot_fees
339 }
340
341 fn get_uncollected_fee_amount(&self) -> QuoteLots {
342 self.unclaimed_quote_lot_fees
343 }
344
345 fn get_registered_traders(&self) -> &dyn OrderedNodeAllocatorMap<MarketTraderId, TraderState> {
346 &self.traders as &dyn OrderedNodeAllocatorMap<MarketTraderId, TraderState>
347 }
348
349 fn get_trader_state(&self, trader_id: &MarketTraderId) -> Option<&TraderState> {
350 self.get_registered_traders().get(trader_id)
351 }
352
353 fn get_trader_state_from_index(&self, index: u32) -> &TraderState {
354 &self.traders.get_node(index).value
355 }
356
357 #[inline(always)]
358 fn get_trader_index(&self, trader_id: &MarketTraderId) -> Option<u32> {
359 let addr = self.traders.get_addr(trader_id);
360 if addr == SENTINEL {
361 None
362 } else {
363 Some(addr)
364 }
365 }
366
367 fn get_trader_id_from_index(&self, trader_index: u32) -> MarketTraderId {
368 self.traders.get_node(trader_index).key
369 }
370
371 #[inline(always)]
372 fn get_book(&self, side: Side) -> &dyn OrderedNodeAllocatorMap<FIFOOrderId, FIFORestingOrder> {
373 match side {
374 Side::Bid => &self.bids,
375 Side::Ask => &self.asks,
376 }
377 }
378}
379
380impl<
381 MarketTraderId: Debug
382 + PartialOrd
383 + Ord
384 + Default
385 + Copy
386 + Clone
387 + Zeroable
388 + Pod
389 + BorshDeserialize
390 + BorshSerialize,
391 const BIDS_SIZE: usize,
392 const ASKS_SIZE: usize,
393 const NUM_SEATS: usize,
394 > WritableMarket<MarketTraderId, FIFOOrderId, FIFORestingOrder, OrderPacket>
395 for FIFOMarket<MarketTraderId, BIDS_SIZE, ASKS_SIZE, NUM_SEATS>
396{
397 fn initialize_with_params(
398 &mut self,
399 tick_size_in_quote_lots_per_base_unit: QuoteLotsPerBaseUnitPerTick,
400 base_lots_per_base_unit: BaseLotsPerBaseUnit,
401 ) {
402 self.initialize_with_params_inner(
403 tick_size_in_quote_lots_per_base_unit,
404 base_lots_per_base_unit,
405 );
406 }
407
408 fn set_fee(&mut self, taker_fee_bps: u64) {
409 self.taker_fee_bps = taker_fee_bps;
410 }
411
412 fn get_registered_traders_mut(
413 &mut self,
414 ) -> &mut dyn OrderedNodeAllocatorMap<MarketTraderId, TraderState> {
415 &mut self.traders as &mut dyn OrderedNodeAllocatorMap<MarketTraderId, TraderState>
416 }
417
418 fn get_trader_state_mut(&mut self, trader_id: &MarketTraderId) -> Option<&mut TraderState> {
419 self.get_registered_traders_mut().get_mut(trader_id)
420 }
421
422 fn get_trader_state_from_index_mut(&mut self, index: u32) -> &mut TraderState {
423 &mut self.traders.get_node_mut(index).value
424 }
425
426 #[inline(always)]
427 fn get_book_mut(
428 &mut self,
429 side: Side,
430 ) -> &mut dyn OrderedNodeAllocatorMap<FIFOOrderId, FIFORestingOrder> {
431 match side {
432 Side::Bid => &mut self.bids,
433 Side::Ask => &mut self.asks,
434 }
435 }
436
437 fn place_order(
438 &mut self,
439 trader_id: &MarketTraderId,
440 order_packet: OrderPacket,
441 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
442 get_clock_fn: &mut dyn FnMut() -> (u64, u64),
443 ) -> Option<(Option<FIFOOrderId>, MatchingEngineResponse)> {
444 self.place_order_inner(trader_id, order_packet, record_event_fn, get_clock_fn)
445 }
446
447 fn reduce_order(
448 &mut self,
449 trader_id: &MarketTraderId,
450 order_id: &FIFOOrderId,
451 side: Side,
452 size: Option<BaseLots>,
453 claim_funds: bool,
454 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
455 ) -> Option<MatchingEngineResponse> {
456 self.reduce_order_inner(
457 self.get_trader_index(trader_id)?,
458 order_id,
459 side,
460 size,
461 false,
462 claim_funds,
463 record_event_fn,
464 )
465 }
466
467 fn cancel_all_orders(
468 &mut self,
469 trader_id: &MarketTraderId,
470 claim_funds: bool,
471 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
472 ) -> Option<MatchingEngineResponse> {
473 self.cancel_all_orders_inner(trader_id, claim_funds, record_event_fn)
474 }
475
476 #[allow(clippy::too_many_arguments)]
477 fn cancel_up_to(
478 &mut self,
479 trader_id: &MarketTraderId,
480 side: Side,
481 num_orders_to_search: Option<usize>,
482 num_orders_to_cancel: Option<usize>,
483 tick_limit: Option<Ticks>,
484 claim_funds: bool,
485 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
486 ) -> Option<MatchingEngineResponse> {
487 self.cancel_up_to_inner(
488 trader_id,
489 side,
490 num_orders_to_search,
491 num_orders_to_cancel,
492 tick_limit,
493 claim_funds,
494 record_event_fn,
495 )
496 }
497
498 fn cancel_multiple_orders_by_id(
499 &mut self,
500 trader_id: &MarketTraderId,
501 orders_to_cancel: &[FIFOOrderId],
502 claim_funds: bool,
503 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
504 ) -> Option<MatchingEngineResponse> {
505 self.cancel_multiple_orders_by_id_inner(
506 self.get_trader_index(trader_id)?,
507 orders_to_cancel,
508 claim_funds,
509 record_event_fn,
510 )
511 }
512
513 fn claim_funds(
514 &mut self,
515 trader_id: &MarketTraderId,
516 num_quote_lots: Option<QuoteLots>,
517 num_base_lots: Option<BaseLots>,
518 allow_seat_eviction: bool,
519 ) -> Option<MatchingEngineResponse> {
520 self.claim_funds_inner(
521 self.get_trader_index(trader_id)?,
522 num_quote_lots,
523 num_base_lots,
524 allow_seat_eviction,
525 )
526 }
527
528 fn collect_fees(
529 &mut self,
530 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
531 ) -> QuoteLots {
532 let quote_lot_fees = self.unclaimed_quote_lot_fees;
533 self.collected_quote_lot_fees += self.unclaimed_quote_lot_fees;
534 self.unclaimed_quote_lot_fees = QuoteLots::ZERO;
535 let fees_collected_in_quote_lots = quote_lot_fees;
536 record_event_fn(MarketEvent::Fee {
537 fees_collected_in_quote_lots,
538 });
539 fees_collected_in_quote_lots
540 }
541}
542
543impl<
544 MarketTraderId: Debug
545 + PartialOrd
546 + Ord
547 + Default
548 + Copy
549 + Clone
550 + Zeroable
551 + Pod
552 + BorshDeserialize
553 + BorshSerialize,
554 const BIDS_SIZE: usize,
555 const ASKS_SIZE: usize,
556 const NUM_SEATS: usize,
557 > FIFOMarket<MarketTraderId, BIDS_SIZE, ASKS_SIZE, NUM_SEATS>
558{
559 pub fn new(
560 tick_size_in_quote_lots_per_base_unit: QuoteLotsPerBaseUnitPerTick,
561 base_lots_per_base_unit: BaseLotsPerBaseUnit,
562 ) -> Self {
563 let mut market = Self::default();
564 market.set_initial_params(
565 tick_size_in_quote_lots_per_base_unit,
566 base_lots_per_base_unit,
567 );
568 market
569 }
570
571 fn initialize(&mut self) {
572 self.bids.initialize();
573 self.asks.initialize();
574 self.traders.initialize();
575 }
576
577 fn initialize_with_params_inner(
578 &mut self,
579 tick_size_in_quote_lots_per_base_unit: QuoteLotsPerBaseUnitPerTick,
580 base_lots_per_base_unit: BaseLotsPerBaseUnit,
581 ) {
582 self.initialize();
583 self.set_initial_params(
584 tick_size_in_quote_lots_per_base_unit,
585 base_lots_per_base_unit,
586 );
587 }
588
589 fn set_initial_params(
590 &mut self,
591 tick_size_in_quote_lots_per_base_unit: QuoteLotsPerBaseUnitPerTick,
592 base_lots_per_base_unit: BaseLotsPerBaseUnit,
593 ) {
594 assert!(tick_size_in_quote_lots_per_base_unit % base_lots_per_base_unit == 0);
595
596 assert_eq!(self.order_sequence_number, 0);
598 self.tick_size_in_quote_lots_per_base_unit = tick_size_in_quote_lots_per_base_unit;
599 self.base_lots_per_base_unit = base_lots_per_base_unit;
600 self.order_sequence_number += 1;
602 }
603
604 #[inline]
605 fn compute_fee(&self, size_in_adjusted_quote_lots: AdjustedQuoteLots) -> AdjustedQuoteLots {
607 AdjustedQuoteLots::new(
608 ((size_in_adjusted_quote_lots.as_u128() * self.taker_fee_bps as u128 + 10000 - 1)
609 / 10000) as u64,
610 )
611 }
612
613 #[inline]
614 fn adjusted_quote_lot_budget_post_fee_adjustment_for_buys(
622 &self,
623 size_in_adjusted_quote_lots: AdjustedQuoteLots,
624 ) -> Option<AdjustedQuoteLots> {
625 let fee_adjustment = self.compute_fee(AdjustedQuoteLots::MAX).as_u128() + u64::MAX as u128;
626 u64::try_from(size_in_adjusted_quote_lots.as_u128() * u64::MAX as u128 / fee_adjustment)
628 .ok()
629 .map(AdjustedQuoteLots::new)
630 }
631
632 #[inline]
633 fn adjusted_quote_lot_budget_post_fee_adjustment_for_sells(
641 &self,
642 size_in_adjusted_quote_lots: AdjustedQuoteLots,
643 ) -> Option<AdjustedQuoteLots> {
644 let fee_adjustment = u64::MAX as u128 - self.compute_fee(AdjustedQuoteLots::MAX).as_u128();
645 u64::try_from(size_in_adjusted_quote_lots.as_u128() * u64::MAX as u128 / fee_adjustment)
647 .ok()
648 .map(AdjustedQuoteLots::new)
649 }
650
651 #[inline]
652 pub fn round_adjusted_quote_lots_up(
654 &self,
655 num_adjusted_quote_lots: AdjustedQuoteLots,
656 ) -> AdjustedQuoteLots {
657 ((num_adjusted_quote_lots
658 + AdjustedQuoteLots::new(self.base_lots_per_base_unit.as_u64() - 1))
659 .unchecked_div::<BaseLotsPerBaseUnit, QuoteLots>(self.base_lots_per_base_unit))
660 * self.base_lots_per_base_unit
661 }
662
663 #[inline]
664 pub fn round_adjusted_quote_lots_down(
666 &self,
667 num_adjusted_quote_lots: AdjustedQuoteLots,
668 ) -> AdjustedQuoteLots {
669 num_adjusted_quote_lots
670 .unchecked_div::<BaseLotsPerBaseUnit, QuoteLots>(self.base_lots_per_base_unit)
671 * self.base_lots_per_base_unit
672 }
673
674 fn check_for_cross(
678 &mut self,
679 side: Side,
680 num_ticks: Ticks,
681 current_slot: u64,
682 current_unix_timestamp_in_seconds: u64,
683 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
684 ) -> Option<Ticks> {
685 loop {
686 let book_entry = self.get_book_mut(side.opposite()).get_min();
687 if let Some((o_id, order)) = book_entry {
688 let crosses = match side.opposite() {
689 Side::Bid => o_id.price_in_ticks >= num_ticks,
690 Side::Ask => o_id.price_in_ticks <= num_ticks,
691 };
692 if !crosses {
693 break;
694 } else if order.num_base_lots > BaseLots::ZERO {
695 if order.is_expired(current_slot, current_unix_timestamp_in_seconds) {
696 self.reduce_order_inner(
697 order.trader_index as u32,
698 &o_id,
699 side.opposite(),
700 None,
701 true,
702 false,
703 record_event_fn,
704 )?;
705 } else {
706 return Some(o_id.price_in_ticks);
707 }
708 } else {
709 phoenix_log!("WARNING: Empty order found in check_for_cross");
712 self.get_book_mut(side.opposite()).remove(&o_id);
713 }
714 } else {
715 break;
717 }
718 }
719 None
720 }
721
722 #[inline(always)]
723 fn claim_funds_inner(
724 &mut self,
725 trader_index: u32,
726 num_quote_lots: Option<QuoteLots>,
727 num_base_lots: Option<BaseLots>,
728 allow_seat_eviction: bool,
729 ) -> Option<MatchingEngineResponse> {
730 if self.get_sequence_number() == 0 {
731 return None;
732 }
733 let (is_empty, quote_lots_received, base_lots_received) = {
734 let trader_state = self.get_trader_state_from_index_mut(trader_index);
735 let quote_lots_free = num_quote_lots
736 .unwrap_or(trader_state.quote_lots_free)
737 .min(trader_state.quote_lots_free);
738 let base_lots_free = num_base_lots
739 .unwrap_or(trader_state.base_lots_free)
740 .min(trader_state.base_lots_free);
741 trader_state.quote_lots_free -= quote_lots_free;
742 trader_state.base_lots_free -= base_lots_free;
743 (
744 *trader_state == TraderState::default(),
745 quote_lots_free,
746 base_lots_free,
747 )
748 };
749 if is_empty && allow_seat_eviction {
750 let trader_id = self.get_trader_id_from_index(trader_index);
751 self.traders.remove(&trader_id);
752 }
753 Some(MatchingEngineResponse::new_withdraw(
754 base_lots_received,
755 quote_lots_received,
756 ))
757 }
758
759 fn place_order_inner(
760 &mut self,
761 trader_id: &MarketTraderId,
762 mut order_packet: OrderPacket,
763 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
764 get_clock_fn: &mut dyn FnMut() -> (u64, u64),
765 ) -> Option<(Option<FIFOOrderId>, MatchingEngineResponse)> {
766 if self.order_sequence_number == 0 {
767 phoenix_log!("Market is uninitialized");
768 return None;
769 }
770 if self.order_sequence_number == u64::MAX >> 1 {
771 phoenix_log!("Sequence number exceeded maximum");
772 return None;
773 }
774
775 let side = order_packet.side();
776 match side {
777 Side::Bid => {
778 if order_packet.get_price_in_ticks() == Ticks::ZERO {
779 phoenix_log!("Bid price is too low");
780 return None;
781 }
782 }
783 Side::Ask => {
784 if !order_packet.is_take_only() {
785 let tick_price = order_packet.get_price_in_ticks();
786 order_packet.set_price_in_ticks(tick_price.max(Ticks::ONE));
787 }
788 }
789 }
790 let trader_index = if order_packet.is_take_only() {
791 self.get_trader_index(trader_id).unwrap_or(u32::MAX)
792 } else {
793 self.get_or_register_trader(trader_id)?
794 };
795
796 if order_packet.num_base_lots() == 0 && order_packet.num_quote_lots() == 0 {
797 phoenix_log!("Either num_base_lots or num_quote_lots must be nonzero");
798 return None;
799 }
800
801 if let OrderPacket::ImmediateOrCancel {
803 num_base_lots,
804 num_quote_lots,
805 ..
806 } = order_packet
807 {
808 if num_base_lots > BaseLots::ZERO && num_quote_lots > QuoteLots::ZERO
809 || num_base_lots == BaseLots::ZERO && num_quote_lots == QuoteLots::ZERO
810 {
811 phoenix_log!(
812 "Invalid IOC params.
813 Exactly one of num_base_lots or num_quote_lots must be nonzero.
814 num_quote_lots: {},
815 num_base_lots: {}",
816 num_quote_lots,
817 num_base_lots
818 );
819 return None;
820 }
821 }
822
823 let (current_slot, current_unix_timestamp) = get_clock_fn();
824
825 if order_packet.is_expired(current_slot, current_unix_timestamp) {
826 phoenix_log!("Order parameters include a last_valid_slot or last_valid_unix_timestamp_in_seconds in the past, skipping matching and posting");
827 return Some((None, MatchingEngineResponse::default()));
829 }
830
831 let (resting_order, mut matching_engine_response) = if let OrderPacket::PostOnly {
832 price_in_ticks,
833 reject_post_only,
834 ..
835 } = &mut order_packet
836 {
837 if let Some(ticks) = self.check_for_cross(
839 side,
840 *price_in_ticks,
841 current_slot,
842 current_unix_timestamp,
843 record_event_fn,
844 ) {
845 if *reject_post_only {
846 phoenix_log!("PostOnly order crosses the book - order rejected");
847 return None;
848 } else {
849 match side {
850 Side::Bid => {
851 if ticks <= Ticks::ONE {
852 phoenix_log!("PostOnly order crosses the book and can not be amended to a valid price - order rejected");
853 return None;
854 }
855 *price_in_ticks = ticks - Ticks::ONE;
856 }
857 Side::Ask => {
858 *price_in_ticks = ticks + Ticks::ONE;
859 }
860 }
861 phoenix_log!("PostOnly order crosses the book - order amended");
862 }
863 }
864
865 (
866 FIFORestingOrder::new(
867 trader_index as u64,
868 order_packet.num_base_lots(),
869 order_packet.get_last_valid_slot(),
870 order_packet.get_last_valid_unix_timestamp_in_seconds(),
871 ),
872 MatchingEngineResponse::default(),
873 )
874 } else {
875 let base_lot_budget = order_packet.base_lot_budget();
876 let quote_lot_budget = order_packet.quote_lot_budget();
879 let adjusted_quote_lot_budget = match side {
880 Side::Bid => quote_lot_budget.and_then(|quote_lot_budget| {
883 self.adjusted_quote_lot_budget_post_fee_adjustment_for_buys(
884 quote_lot_budget * self.base_lots_per_base_unit,
885 )
886 }),
887 Side::Ask => quote_lot_budget.and_then(|quote_lot_budget| {
890 self.adjusted_quote_lot_budget_post_fee_adjustment_for_sells(
891 quote_lot_budget * self.base_lots_per_base_unit,
892 )
893 }),
894 }
895 .unwrap_or_else(|| AdjustedQuoteLots::new(u64::MAX));
896
897 let mut inflight_order = InflightOrder::new(
898 side,
899 order_packet.self_trade_behavior(),
900 order_packet.get_price_in_ticks(),
901 order_packet.match_limit(),
902 base_lot_budget,
903 adjusted_quote_lot_budget,
904 order_packet.get_last_valid_slot(),
905 order_packet.get_last_valid_unix_timestamp_in_seconds(),
906 );
907 let resting_order = self
908 .match_order(
909 &mut inflight_order,
910 trader_index,
911 record_event_fn,
912 current_slot,
913 current_unix_timestamp,
914 )
915 .map_or_else(
916 || {
917 phoenix_log!("Encountered error matching order");
918 None
919 },
920 Some,
921 )?;
922 let matched_quote_lots = match side {
925 Side::Bid => {
927 (self.round_adjusted_quote_lots_up(inflight_order.matched_adjusted_quote_lots)
928 / self.base_lots_per_base_unit)
929 + inflight_order.quote_lot_fees
930 }
931 Side::Ask => {
933 (self
934 .round_adjusted_quote_lots_down(inflight_order.matched_adjusted_quote_lots)
935 / self.base_lots_per_base_unit)
936 - inflight_order.quote_lot_fees
937 }
938 };
939 let matching_engine_response = match side {
940 Side::Bid => MatchingEngineResponse::new_from_buy(
941 matched_quote_lots,
942 inflight_order.matched_base_lots,
943 ),
944 Side::Ask => MatchingEngineResponse::new_from_sell(
945 inflight_order.matched_base_lots,
946 matched_quote_lots,
947 ),
948 };
949
950 record_event_fn(MarketEvent::FillSummary {
951 client_order_id: order_packet.client_order_id(),
952 total_base_lots_filled: inflight_order.matched_base_lots,
953 total_quote_lots_filled: matched_quote_lots,
954 total_fee_in_quote_lots: inflight_order.quote_lot_fees,
955 });
956
957 (resting_order, matching_engine_response)
958 };
959
960 let mut placed_order_id = None;
961
962 if let OrderPacket::ImmediateOrCancel {
963 min_base_lots_to_fill,
964 min_quote_lots_to_fill,
965 ..
966 } = order_packet
967 {
968 if matching_engine_response.num_base_lots() < min_base_lots_to_fill
971 || matching_engine_response.num_quote_lots() < min_quote_lots_to_fill
972 {
973 phoenix_log!(
974 "IOC order failed to meet minimum fill requirements.
975 min_base_lots_to_fill: {},
976 min_quote_lots_to_fill: {},
977 matched_base_lots: {},
978 matched_quote_lots: {}",
979 min_base_lots_to_fill,
980 min_quote_lots_to_fill,
981 matching_engine_response.num_base_lots(),
982 matching_engine_response.num_quote_lots(),
983 );
984 return None;
985 }
986 } else {
987 let price_in_ticks = order_packet.get_price_in_ticks();
988 let (order_id, book_full) = match side {
989 Side::Bid => (
990 FIFOOrderId::new(price_in_ticks, !self.order_sequence_number),
991 self.bids.len() == self.bids.capacity(),
992 ),
993 Side::Ask => (
994 FIFOOrderId::new(price_in_ticks, self.order_sequence_number),
995 self.asks.len() == self.asks.capacity(),
996 ),
997 };
998
999 let limit_order_crosses = if matches!(order_packet, OrderPacket::PostOnly { .. }) {
1000 false
1002 } else {
1003 let best_price_on_opposite_book = self
1005 .get_book(side.opposite())
1006 .iter()
1007 .find(|(_, resting_order)| {
1008 !resting_order.is_expired(current_slot, current_unix_timestamp)
1009 && resting_order.num_base_lots > BaseLots::ZERO
1010 })
1011 .map(|(o_id, _)| o_id.price_in_ticks)
1012 .unwrap_or_else(|| match side {
1013 Side::Bid => Ticks::MAX,
1014 Side::Ask => Ticks::ZERO,
1015 });
1016 match side {
1017 Side::Bid => order_packet.get_price_in_ticks() >= best_price_on_opposite_book,
1018 Side::Ask => order_packet.get_price_in_ticks() <= best_price_on_opposite_book,
1019 }
1020 };
1021
1022 if resting_order.num_base_lots > BaseLots::ZERO && !limit_order_crosses {
1024 placed_order_id = Some(order_id);
1026 if book_full {
1027 phoenix_log!("Book is full. Evicting order");
1028 self.evict_least_aggressive_order(side, record_event_fn, &order_id);
1029 }
1030 self.get_book_mut(side)
1032 .insert(order_id, resting_order)
1033 .map_or_else(
1034 || {
1035 phoenix_log!("Failed to insert order into book");
1036 None
1037 },
1038 Some,
1039 )?;
1040 let tick_size_in_quote_lots_per_base_unit =
1042 self.tick_size_in_quote_lots_per_base_unit;
1043 let base_lots_per_base_unit = self.base_lots_per_base_unit;
1044 let trader_state = self.get_trader_state_from_index_mut(trader_index);
1045 match side {
1047 Side::Bid => {
1048 let quote_lots_to_lock = (tick_size_in_quote_lots_per_base_unit
1049 * order_id.price_in_ticks
1050 * resting_order.num_base_lots)
1051 / base_lots_per_base_unit;
1052 let quote_lots_free_to_use =
1053 quote_lots_to_lock.min(trader_state.quote_lots_free);
1054 trader_state.use_free_quote_lots(quote_lots_free_to_use);
1055 trader_state.lock_quote_lots(quote_lots_to_lock);
1056 matching_engine_response.post_quote_lots(quote_lots_to_lock);
1057 matching_engine_response.use_free_quote_lots(quote_lots_free_to_use);
1058 }
1059 Side::Ask => {
1060 let base_lots_free_to_use =
1061 resting_order.num_base_lots.min(trader_state.base_lots_free);
1062 trader_state.use_free_base_lots(base_lots_free_to_use);
1063 trader_state.lock_base_lots(resting_order.num_base_lots);
1064 matching_engine_response.post_base_lots(resting_order.num_base_lots);
1065 matching_engine_response.use_free_base_lots(base_lots_free_to_use);
1066 }
1067 }
1068
1069 record_event_fn(MarketEvent::<MarketTraderId>::Place {
1071 order_sequence_number: order_id.order_sequence_number,
1072 price_in_ticks: order_id.price_in_ticks,
1073 base_lots_placed: resting_order.num_base_lots,
1074 client_order_id: order_packet.client_order_id(),
1075 });
1076
1077 if resting_order.last_valid_slot != 0
1078 || resting_order.last_valid_unix_timestamp_in_seconds != 0
1079 {
1080 record_event_fn(MarketEvent::<MarketTraderId>::TimeInForce {
1082 order_sequence_number: order_id.order_sequence_number,
1083 last_valid_slot: resting_order.last_valid_slot,
1084 last_valid_unix_timestamp_in_seconds: resting_order
1085 .last_valid_unix_timestamp_in_seconds,
1086 });
1087 }
1088
1089 self.order_sequence_number += 1;
1091 }
1092 }
1093
1094 if trader_index != u32::MAX {
1096 let trader_state = self.get_trader_state_from_index_mut(trader_index);
1097 match side {
1098 Side::Bid => {
1099 let quote_lots_free_to_use = trader_state
1100 .quote_lots_free
1101 .min(matching_engine_response.num_quote_lots());
1102 trader_state.use_free_quote_lots(quote_lots_free_to_use);
1103 matching_engine_response.use_free_quote_lots(quote_lots_free_to_use);
1104 }
1105 Side::Ask => {
1106 let base_lots_free_to_use = trader_state
1107 .base_lots_free
1108 .min(matching_engine_response.num_base_lots());
1109 trader_state.use_free_base_lots(base_lots_free_to_use);
1110 matching_engine_response.use_free_base_lots(base_lots_free_to_use);
1111 }
1112 }
1113
1114 if order_packet.no_deposit_or_withdrawal() {
1117 match side {
1118 Side::Bid => {
1119 trader_state
1120 .deposit_free_base_lots(matching_engine_response.num_base_lots_out);
1121 matching_engine_response.num_base_lots_out = BaseLots::ZERO;
1122 }
1123 Side::Ask => {
1124 trader_state
1125 .deposit_free_quote_lots(matching_engine_response.num_quote_lots_out);
1126 matching_engine_response.num_quote_lots_out = QuoteLots::ZERO;
1127 }
1128 }
1129
1130 if !matching_engine_response.verify_no_deposit() {
1132 phoenix_log!("Trader does not have enough deposited funds to process order");
1133 return None;
1134 }
1135
1136 if !matching_engine_response.verify_no_withdrawal() {
1138 phoenix_log!("Matching engine response withdraws base or quote lots");
1139 return None;
1140 }
1141 }
1142 }
1143
1144 Some((placed_order_id, matching_engine_response))
1145 }
1146
1147 fn evict_least_aggressive_order(
1148 &mut self,
1149 side: Side,
1150 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
1151 placed_order_id: &FIFOOrderId,
1152 ) -> Option<FIFORestingOrder> {
1153 let (order_id, resting_order) = {
1154 let (fifo_order_id, resting_order) = self.get_book_mut(side).get_max()?;
1156 let maker_id = self.get_trader_id_from_index(resting_order.trader_index as u32);
1157 if match side {
1158 Side::Bid => fifo_order_id.price_in_ticks >= placed_order_id.price_in_ticks,
1159 Side::Ask => fifo_order_id.price_in_ticks <= placed_order_id.price_in_ticks,
1160 } {
1161 phoenix_log!("New order is not aggressive enough to evict an existing order");
1162 return None;
1163 }
1164 self.get_book_mut(side).remove(&fifo_order_id)?;
1165 record_event_fn(MarketEvent::<MarketTraderId>::Evict {
1166 maker_id,
1167 order_sequence_number: fifo_order_id.order_sequence_number,
1168 price_in_ticks: fifo_order_id.price_in_ticks,
1169 base_lots_evicted: resting_order.num_base_lots,
1170 });
1171 (fifo_order_id, resting_order)
1172 };
1173 let tick_size_in_quote_lots_per_base_unit = self.tick_size_in_quote_lots_per_base_unit;
1175 let base_lots_per_base_unit = self.base_lots_per_base_unit;
1176 let trader_state = self.get_trader_state_from_index_mut(resting_order.trader_index as u32);
1177 match side {
1178 Side::Bid => {
1179 let quote_lots_to_unlock = (order_id.price_in_ticks
1180 * tick_size_in_quote_lots_per_base_unit
1181 * resting_order.num_base_lots)
1182 / base_lots_per_base_unit;
1183 trader_state.unlock_quote_lots(quote_lots_to_unlock);
1184 }
1185 Side::Ask => trader_state.unlock_base_lots(resting_order.num_base_lots),
1186 }
1187 Some(resting_order)
1188 }
1189
1190 fn match_order(
1191 &mut self,
1192 inflight_order: &mut InflightOrder,
1193 current_trader_index: u32,
1194 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
1195 current_slot: u64,
1196 current_unix_timestamp: u64,
1197 ) -> Option<FIFORestingOrder> {
1198 let mut total_matched_adjusted_quote_lots = AdjustedQuoteLots::ZERO;
1199 while inflight_order.in_progress() {
1200 let (
1202 trader_index,
1203 order_id,
1204 num_base_lots_quoted,
1205 last_valid_slot,
1206 last_valid_unix_timestamp_in_seconds,
1207 ) = {
1208 let book = self.get_book_mut(inflight_order.side.opposite());
1209 let (
1211 crossed,
1212 order_id,
1213 FIFORestingOrder {
1214 trader_index,
1215 num_base_lots: num_base_lots_quoted,
1216 last_valid_slot,
1217 last_valid_unix_timestamp_in_seconds,
1218 },
1219 ) = if let Some((o_id, quote)) = book.get_min() {
1220 (
1221 match inflight_order.side {
1222 Side::Bid => o_id.price_in_ticks <= inflight_order.limit_price_in_ticks,
1223 Side::Ask => o_id.price_in_ticks >= inflight_order.limit_price_in_ticks,
1224 },
1225 o_id,
1226 quote,
1227 )
1228 } else {
1229 phoenix_log!("Book is empty");
1230 break;
1231 };
1232 if !crossed {
1234 break;
1235 }
1236 if num_base_lots_quoted == BaseLots::ZERO {
1237 book.remove(&order_id)?;
1240 inflight_order.match_limit -= 1;
1242 continue;
1243 }
1244 (
1245 trader_index,
1246 order_id,
1247 num_base_lots_quoted,
1248 last_valid_slot,
1249 last_valid_unix_timestamp_in_seconds,
1250 )
1251 };
1252
1253 if (last_valid_slot != 0 && last_valid_slot < current_slot)
1256 || (last_valid_unix_timestamp_in_seconds != 0
1257 && last_valid_unix_timestamp_in_seconds < current_unix_timestamp)
1258 {
1259 self.reduce_order_inner(
1260 trader_index as u32,
1261 &order_id,
1262 inflight_order.side.opposite(),
1263 None,
1264 true,
1265 false,
1266 record_event_fn,
1267 )?;
1268 inflight_order.match_limit -= 1;
1269 continue;
1270 }
1271
1272 if trader_index == current_trader_index as u64 {
1274 match inflight_order.self_trade_behavior {
1275 SelfTradeBehavior::Abort => return None,
1276 SelfTradeBehavior::CancelProvide => {
1277 self.reduce_order_inner(
1283 current_trader_index,
1284 &order_id,
1285 inflight_order.side.opposite(),
1286 None,
1287 false,
1288 false,
1289 record_event_fn,
1290 )?;
1291 inflight_order.match_limit -= 1;
1292 }
1293 SelfTradeBehavior::DecrementTake => {
1294 let base_lots_removed = inflight_order
1295 .base_lot_budget
1296 .min(
1297 inflight_order
1298 .adjusted_quote_lot_budget
1299 .unchecked_div::<QuoteLotsPerBaseUnit, BaseLots>(
1300 order_id.price_in_ticks
1301 * self.tick_size_in_quote_lots_per_base_unit,
1302 ),
1303 )
1304 .min(num_base_lots_quoted);
1305
1306 self.reduce_order_inner(
1307 current_trader_index,
1308 &order_id,
1309 inflight_order.side.opposite(),
1310 Some(base_lots_removed),
1311 false,
1312 false,
1313 record_event_fn,
1314 )?;
1315 inflight_order.base_lot_budget = inflight_order
1318 .base_lot_budget
1319 .saturating_sub(base_lots_removed);
1320 inflight_order.adjusted_quote_lot_budget =
1321 inflight_order.adjusted_quote_lot_budget.saturating_sub(
1322 self.tick_size_in_quote_lots_per_base_unit
1323 * order_id.price_in_ticks
1324 * base_lots_removed,
1325 );
1326 inflight_order.match_limit -= 1;
1328 inflight_order.should_terminate = base_lots_removed < num_base_lots_quoted;
1331 }
1332 }
1333 continue;
1334 }
1335
1336 let num_adjusted_quote_lots_quoted = order_id.price_in_ticks
1337 * self.tick_size_in_quote_lots_per_base_unit
1338 * num_base_lots_quoted;
1339
1340 let (matched_base_lots, matched_adjusted_quote_lots, order_remaining_base_lots) = {
1341 let tick_size_in_quote_lots_per_base_unit =
1343 self.tick_size_in_quote_lots_per_base_unit;
1344
1345 let book = self.get_book_mut(inflight_order.side.opposite());
1346
1347 let has_remaining_adjusted_quote_lots =
1349 num_adjusted_quote_lots_quoted <= inflight_order.adjusted_quote_lot_budget;
1350 let has_remaining_base_lots =
1351 num_base_lots_quoted <= inflight_order.base_lot_budget;
1352
1353 if has_remaining_base_lots && has_remaining_adjusted_quote_lots {
1354 book.remove(&order_id)?;
1356 (
1357 num_base_lots_quoted,
1358 num_adjusted_quote_lots_quoted,
1359 BaseLots::ZERO,
1360 )
1361 } else {
1362 let base_lots_to_remove = inflight_order.base_lot_budget.min(
1364 inflight_order
1365 .adjusted_quote_lot_budget
1366 .unchecked_div::<QuoteLotsPerBaseUnit, BaseLots>(
1367 order_id.price_in_ticks * tick_size_in_quote_lots_per_base_unit,
1368 ),
1369 );
1370 let adjusted_quote_lots_to_remove = order_id.price_in_ticks
1371 * tick_size_in_quote_lots_per_base_unit
1372 * base_lots_to_remove;
1373 let matched_order = book.get_mut(&order_id)?;
1374 matched_order.num_base_lots -= base_lots_to_remove;
1375 inflight_order.should_terminate = true;
1378 (
1379 base_lots_to_remove,
1380 adjusted_quote_lots_to_remove,
1381 matched_order.num_base_lots,
1382 )
1383 }
1384 };
1385
1386 inflight_order.process_match(matched_adjusted_quote_lots, matched_base_lots);
1388
1389 total_matched_adjusted_quote_lots += matched_adjusted_quote_lots;
1391
1392 if matched_base_lots != BaseLots::ZERO {
1394 record_event_fn(MarketEvent::<MarketTraderId>::Fill {
1396 maker_id: self.get_trader_id_from_index(trader_index as u32),
1397 order_sequence_number: order_id.order_sequence_number,
1398 price_in_ticks: order_id.price_in_ticks,
1399 base_lots_filled: matched_base_lots,
1400 base_lots_remaining: order_remaining_base_lots,
1401 });
1402 } else if !inflight_order.should_terminate {
1403 phoenix_log!(
1404 "WARNING: should_terminate should always be true if matched_base_lots is zero"
1405 );
1406 }
1407
1408 let base_lots_per_base_unit = self.base_lots_per_base_unit;
1409 let trader_state = self.get_trader_state_from_index_mut(trader_index as u32);
1411 match inflight_order.side {
1412 Side::Bid => trader_state.process_limit_sell(
1413 matched_base_lots,
1414 matched_adjusted_quote_lots / base_lots_per_base_unit,
1415 ),
1416 Side::Ask => trader_state.process_limit_buy(
1417 matched_adjusted_quote_lots / base_lots_per_base_unit,
1418 matched_base_lots,
1419 ),
1420 }
1421 }
1422 inflight_order.quote_lot_fees = self
1424 .round_adjusted_quote_lots_up(self.compute_fee(total_matched_adjusted_quote_lots))
1425 / self.base_lots_per_base_unit;
1426 self.unclaimed_quote_lot_fees += inflight_order.quote_lot_fees;
1427
1428 Some(FIFORestingOrder::new(
1429 current_trader_index as u64,
1430 inflight_order.base_lot_budget,
1431 inflight_order.last_valid_slot,
1432 inflight_order.last_valid_unix_timestamp_in_seconds,
1433 ))
1434 }
1435
1436 fn cancel_all_orders_inner(
1437 &mut self,
1438 trader_id: &MarketTraderId,
1439 claim_funds: bool,
1440 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
1441 ) -> Option<MatchingEngineResponse> {
1442 let trader_index = self.get_trader_index(trader_id)?;
1443 let orders_to_cancel = [Side::Bid, Side::Ask]
1444 .iter()
1445 .flat_map(|side| {
1446 self.get_book(*side)
1447 .iter()
1448 .filter(|(_o_id, o)| {
1449 o.trader_index == trader_index as u64 && o.num_base_lots > BaseLots::ZERO
1450 })
1451 .map(|(o_id, _)| *o_id)
1452 })
1453 .collect::<Vec<_>>();
1454 self.cancel_multiple_orders_by_id_inner(
1455 trader_index,
1456 &orders_to_cancel,
1457 claim_funds,
1458 record_event_fn,
1459 )
1460 }
1461
1462 #[allow(clippy::too_many_arguments)]
1463 fn cancel_up_to_inner(
1464 &mut self,
1465 trader_id: &MarketTraderId,
1466 side: Side,
1467 num_orders_to_search: Option<usize>,
1468 num_orders_to_cancel: Option<usize>,
1469 tick_limit: Option<Ticks>,
1470 claim_funds: bool,
1471 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
1472 ) -> Option<MatchingEngineResponse> {
1473 let trader_index = self.get_trader_index(trader_id)?;
1474
1475 let last_tick = tick_limit.unwrap_or(match side {
1476 Side::Ask => Ticks::MAX,
1477 Side::Bid => Ticks::MIN,
1478 });
1479 let book = self.get_book(side);
1480 let num_orders = book.len();
1481
1482 let orders_to_cancel = book
1483 .iter()
1484 .take(num_orders_to_search.unwrap_or(num_orders))
1485 .filter(|(_o_id, o)| o.trader_index == trader_index as u64)
1486 .filter(|(o_id, _)| match side {
1487 Side::Bid => o_id.price_in_ticks >= last_tick,
1488 Side::Ask => o_id.price_in_ticks <= last_tick,
1489 })
1490 .take(num_orders_to_cancel.unwrap_or(num_orders))
1491 .map(|(o_id, _)| *o_id)
1492 .collect::<Vec<_>>();
1493
1494 self.cancel_multiple_orders_by_id_inner(
1495 trader_index,
1496 &orders_to_cancel,
1497 claim_funds,
1498 record_event_fn,
1499 )
1500 }
1501
1502 fn cancel_multiple_orders_by_id_inner(
1503 &mut self,
1504 trader_index: u32,
1505 orders_to_cancel: &[FIFOOrderId],
1506 claim_funds: bool,
1507 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
1508 ) -> Option<MatchingEngineResponse> {
1509 let (quote_lots_released, base_lots_released) = orders_to_cancel
1510 .iter()
1511 .filter_map(|&order_id| {
1512 self.reduce_order_inner(
1513 trader_index,
1514 &order_id,
1515 Side::from_order_sequence_number(order_id.order_sequence_number),
1516 None,
1517 false,
1518 claim_funds,
1519 record_event_fn,
1520 )
1521 .map(
1522 |MatchingEngineResponse {
1523 num_quote_lots_out,
1524 num_base_lots_out,
1525 ..
1526 }| (num_quote_lots_out, num_base_lots_out),
1527 )
1528 })
1529 .fold(
1530 (QuoteLots::ZERO, BaseLots::ZERO),
1531 |(quote_lots_released, base_lots_released), (quote_lots_out, base_lots_out)| {
1532 (
1533 quote_lots_released + quote_lots_out,
1534 base_lots_released + base_lots_out,
1535 )
1536 },
1537 );
1538
1539 Some(MatchingEngineResponse::new_withdraw(
1540 base_lots_released,
1541 quote_lots_released,
1542 ))
1543 }
1544
1545 #[allow(clippy::too_many_arguments)]
1546 #[inline(always)]
1547 fn reduce_order_inner(
1548 &mut self,
1549 trader_index: u32,
1550 order_id: &FIFOOrderId,
1551 side: Side,
1552 size: Option<BaseLots>,
1553 order_is_expired: bool,
1554 claim_funds: bool,
1555 record_event_fn: &mut dyn FnMut(MarketEvent<MarketTraderId>),
1556 ) -> Option<MatchingEngineResponse> {
1557 let maker_id = self.get_trader_id_from_index(trader_index);
1558 let removed_base_lots = {
1559 let book = self.get_book_mut(side);
1560 let (should_remove_order_from_book, base_lots_to_remove) = {
1561 if let Some(order) = book.get(order_id) {
1562 let base_lots_to_remove = size
1563 .map(|s| s.min(order.num_base_lots))
1564 .unwrap_or(order.num_base_lots);
1565 if order.trader_index != trader_index as u64 {
1566 return None;
1567 }
1568 if order_is_expired {
1570 (true, order.num_base_lots)
1571 } else {
1572 (
1573 base_lots_to_remove == order.num_base_lots,
1574 base_lots_to_remove,
1575 )
1576 }
1577 } else {
1578 return Some(MatchingEngineResponse::default());
1579 }
1580 };
1581 let base_lots_remaining = if should_remove_order_from_book {
1582 book.remove(order_id)?;
1584 BaseLots::ZERO
1585 } else {
1586 let resting_order = book.get_mut(order_id)?;
1588 resting_order.num_base_lots -= base_lots_to_remove;
1589 resting_order.num_base_lots
1590 };
1591 if order_is_expired {
1593 record_event_fn(MarketEvent::ExpiredOrder {
1594 maker_id,
1595 order_sequence_number: order_id.order_sequence_number,
1596 price_in_ticks: order_id.price_in_ticks,
1597 base_lots_removed: base_lots_to_remove,
1598 });
1599 } else {
1600 record_event_fn(MarketEvent::Reduce {
1601 order_sequence_number: order_id.order_sequence_number,
1602 price_in_ticks: order_id.price_in_ticks,
1603 base_lots_removed: base_lots_to_remove,
1604 base_lots_remaining,
1605 });
1606 }
1607 base_lots_to_remove
1608 };
1609 let (num_quote_lots, num_base_lots) = {
1610 let tick_size_in_quote_lots_per_base_unit = self.tick_size_in_quote_lots_per_base_unit;
1612 let base_lots_per_base_unit = self.base_lots_per_base_unit;
1613 let trader_state = self.get_trader_state_from_index_mut(trader_index);
1614 match side {
1615 Side::Bid => {
1616 let quote_lots = (order_id.price_in_ticks
1617 * tick_size_in_quote_lots_per_base_unit
1618 * removed_base_lots)
1619 / base_lots_per_base_unit;
1620 trader_state.unlock_quote_lots(quote_lots);
1621 (quote_lots, BaseLots::ZERO)
1622 }
1623 Side::Ask => {
1624 trader_state.unlock_base_lots(removed_base_lots);
1625 (QuoteLots::ZERO, removed_base_lots)
1626 }
1627 }
1628 };
1629 if claim_funds {
1632 self.claim_funds_inner(
1633 trader_index,
1634 Some(num_quote_lots),
1635 Some(num_base_lots),
1636 false,
1637 )
1638 } else {
1639 Some(MatchingEngineResponse::default())
1640 }
1641 }
1642}