phoenix/state/markets/
fifo.rs

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    /// The price of the order, in ticks. Each market has a designated
30    /// tick size (some number of quote lots per base unit) that is used to convert the price to ticks.
31    /// For example, if the tick size is 0.01, then a price of 1.23 is converted to 123 ticks.
32    /// If the quote lot size is 0.001, this means that there is a spacing of 10 quote lots
33    /// in between each tick.
34    pub price_in_ticks: Ticks,
35
36    /// This is the unique identifier of the order, which is used to determine the side of the order.
37    /// It is derived from the sequence number of the market.
38    ///
39    /// If the order is a bid, the sequence number will have its bits inverted, and if it is an ask,
40    /// the sequence number will be used as is.
41    ///
42    /// The way to identify the side of the order is to check the leading bit of `order_id`.
43    /// A leading bit of 0 indicates an ask, and a leading bit of 1 indicates a bid. See Side::from_order_id.
44    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        // The ordering of the `FIFOOrderId` struct is determined by the price of the order. If the price is the same,
72        // then the order with the lower sequence number is considered to be the lower order.
73        //
74        // Asks are sorted in ascending order, and bids are sorted in descending order.
75        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, // Number of base lots quoted
108    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    /// Padding
209    pub _padding: [u64; 32],
210
211    /// Number of base lots in a base unit. For example, if the lot size is 0.001 SOL, then base_lots_per_base_unit is 1000.
212    pub base_lots_per_base_unit: BaseLotsPerBaseUnit,
213
214    /// Tick size in quote lots per base unit. For example, if the tick size is 0.01 USDC and the quote lot size is 0.001 USDC, then tick_size_in_quote_lots_per_base_unit is 10.
215    pub tick_size_in_quote_lots_per_base_unit: QuoteLotsPerBaseUnitPerTick,
216
217    /// The sequence number of the next event.
218    order_sequence_number: u64,
219
220    /// There are no maker fees. Taker fees are charged on the quote lots transacted in the trade, in basis points.
221    pub taker_fee_bps: u64,
222
223    /// Amount of fees collected from the market in its lifetime, in quote lots.
224    collected_quote_lot_fees: QuoteLots,
225
226    /// Amount of unclaimed fees accrued to the market, in quote lots.
227    unclaimed_quote_lot_fees: QuoteLots,
228
229    /// Red-black tree representing the bids in the order book.
230    pub bids: RedBlackTree<FIFOOrderId, FIFORestingOrder, BIDS_SIZE>,
231
232    /// Red-black tree representing the asks in the order book.
233    pub asks: RedBlackTree<FIFOOrderId, FIFORestingOrder, ASKS_SIZE>,
234
235    /// Red-black tree representing the authorized makers in the market.
236    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        // Ensure there is no re-entrancy
597        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        // After setting the initial params, this function can never be called again
601        self.order_sequence_number += 1;
602    }
603
604    #[inline]
605    /// Round up the fee to the nearest adjusted quote lot
606    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    /// Quote lot budget with fees adjusted (buys)
615    ///
616    /// The desired result is adjusted_quote_lots / (1 + fee_bps). We approach this result by taking
617    /// (size_in_lots * u64::MAX) / (u64::MAX * (1 + fee_bps)) for accurate numerical precision.
618    /// This will never overflow at any point in the calculation because all intermediate values
619    /// will be stored in a u128. There is only a single multiplication of u64's which will be
620    /// strictly less than u128::MAX
621    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        // Return an option to catch truncation from downcasting to u64
627        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    /// Quote lot budget with fees adjusted (sells)
634    ///
635    /// The desired result is adjusted_quote_lots / (1 - fee_bps). We approach this result by taking
636    /// (size_in_lots * u64::MAX) / (u64::MAX * (1 - fee_bps)) for accurate numerical precision.
637    /// This will never overflow at any point in the calculation because all intermediate values
638    /// will be stored in a u128. There is only a single multiplication of u64's which will be
639    /// strictly less than u128::MAX
640    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        // Return an option to catch truncation from downcasting to u64
646        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    /// Adjusted quote lots, rounded up to the nearest multiple of base_lots_per_base_unit
653    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    /// Adjusted quote lots, rounded down to the nearest multiple of base_lots_per_base_unit
665    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    /// This function determines whether a PostOnly order crosses the book.
675    /// If the order crosses the book, the function returns the price of the best unexpired order
676    /// on the opposite side of the book in Ticks. Otherwise, it returns None.
677    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                    // If the order is empty, we can remove it from the tree
710                    // This case should never occur in v1
711                    phoenix_log!("WARNING: Empty order found in check_for_cross");
712                    self.get_book_mut(side.opposite()).remove(&o_id);
713                }
714            } else {
715                // Book is empty
716                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        // For IOC order types exactly one of num_quote_lots or num_base_lots needs to be specified.
802        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            // Do not fail the transaction if the order is expired, but do not place or match the order
828            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            // Handle cases where PostOnly order would cross the book
838            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            // Multiply the quote lot budget by the number of base lots per unit to get the number of
877            // adjusted quote lots (quote_lots * base_lots_per_base_unit)
878            let quote_lot_budget = order_packet.quote_lot_budget();
879            let adjusted_quote_lot_budget = match side {
880                // For buys, the adjusted quote lot budget is decreased by the max fee.
881                // This is because the fee is added to the quote lots spent after the matching is complete.
882                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                // For sells, the adjusted quote lot budget is increased by the max fee.
888                // This is because the fee is subtracted from the quote lot received after the matching is complete.
889                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            // matched_adjusted_quote_lots is rounded down to the nearest tick for buys and up for
923            // sells to yield a whole number of matched_quote_lots.
924            let matched_quote_lots = match side {
925                // We add the quote_lot_fees to account for the fee being paid on a buy order
926                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                // We subtract the quote_lot_fees to account for the fee being paid on a sell order
932                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            // For IOC orders, if the order's minimum fill requirements are not met, then
969            // the order is voided
970            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                // This check has already been performed for PostOnly orders
1001                false
1002            } else {
1003                // Finds the most competitive valid resting order on the opposite book
1004                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            // Only place an order if there is more size to place and the limit order doesn't cross the book
1023            if resting_order.num_base_lots > BaseLots::ZERO && !limit_order_crosses {
1024                // Evict order from the book if it is at capacity
1025                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                // Add new order to the book
1031                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                // These constants need to be copied because we mutably borrow below
1041                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                // Update trader state and matching engine response accordingly
1046                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 the place event
1070                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 the time in force event
1081                    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                // Increment the order sequence number after successfully placing an order
1090                self.order_sequence_number += 1;
1091            }
1092        }
1093
1094        // If the trader is a registered trader, check if they have free lots
1095        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 the order crosses and only uses deposited funds, then add the matched funds back to the trader's free funds
1115            // Set the matching_engine_response lots_out to zero to set token withdrawals to zero
1116            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                // Check if trader has enough deposited funds to process the order
1131                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                // Check that the matching engine response does not withdraw any base or quote lots
1137                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            // Find the least aggressive order in the book
1155            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        // These constants need to be copied because we mutably borrow below
1174        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            // Find the first order on the opposite side of the book that matches the inflight order.
1201            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                // Look at the top of the book to compare the book's price to the order's price
1210                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 the order no longer crosses the limit price (based on limit_price_in_ticks), stop matching
1233                if !crossed {
1234                    break;
1235                }
1236                if num_base_lots_quoted == BaseLots::ZERO {
1237                    // This block is entered if we encounter tombstoned orders during the matching process
1238                    // (Should never trigger in v1)
1239                    book.remove(&order_id)?;
1240                    // The tombstone should count as part of the match limit
1241                    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            // This block is entered if the order has expired. The order is removed from the book and
1254            // the match limit is decremented.
1255            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            // Handle self trade
1273            if trader_index == current_trader_index as u64 {
1274                match inflight_order.self_trade_behavior {
1275                    SelfTradeBehavior::Abort => return None,
1276                    SelfTradeBehavior::CancelProvide => {
1277                        // This block is entered if the self trade behavior for the crossing order is
1278                        // CancelProvide
1279                        //
1280                        // We cancel the order from the book and free up the locked quote_lots or base_lots, but
1281                        // we do not claim them as part of the match
1282                        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                        // In the case that the self trade behavior is DecrementTake, we decrement the
1316                        // the base lot and adjusted quote lot budgets accordingly
1317                        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                        // Self trades will count towards the match limit
1327                        inflight_order.match_limit -= 1;
1328                        // If base_lots_removed < num_base_lots_quoted, then the order budget must be fully
1329                        // exhausted
1330                        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                // This constant needs to be copied because we mutably borrow below
1342                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                // Check if the inflight order's budget is exhausted
1348                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                    // If there is remaining budget, we match the entire book order
1355                    book.remove(&order_id)?;
1356                    (
1357                        num_base_lots_quoted,
1358                        num_adjusted_quote_lots_quoted,
1359                        BaseLots::ZERO,
1360                    )
1361                } else {
1362                    // If the order's budget is exhausted, we match as much as we can
1363                    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                    // If this clause is reached, we make ensure that the loop terminates
1376                    // as the order has been fully filled
1377                    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            // Deplete the inflight order's budget by the amount matched
1387            inflight_order.process_match(matched_adjusted_quote_lots, matched_base_lots);
1388
1389            // Increment the matched adjusted quote lots for fee calculation
1390            total_matched_adjusted_quote_lots += matched_adjusted_quote_lots;
1391
1392            // If the matched base lots is zero, we don't record the fill event
1393            if matched_base_lots != BaseLots::ZERO {
1394                // The fill event is recorded
1395                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            // Update the maker's state to reflect the match
1410            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        // Fees are updated based on the total amount matched
1423        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 the order is tagged as expired, we remove it from the book regardless of the size.
1569                    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                // This will never return None because we already checked that the order exists
1583                book.remove(order_id)?;
1584                BaseLots::ZERO
1585            } else {
1586                // This will never return None because we already checked that the order exists
1587                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 the order was not cancelled by the maker, we make sure that the maker's id is logged.
1592            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            // These constants need to be copied because we mutably borrow below
1611            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        // We don't want to claim funds if an order is removed from the book during a self trade
1630        // or if the user specifically indicates that they don't want to claim funds.
1631        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}