1use std::{
17 cell::RefCell,
18 fmt::{Debug, Display},
19 rc::Rc,
20};
21
22#[cfg(all(feature = "simulation", madsim))]
23use madsim::rand::RngCore;
24use nautilus_core::{
25 UnixNanos,
26 correctness::{check_in_range_inclusive_f64, check_non_negative_f64},
27};
28use nautilus_model::{
29 data::order::BookOrder,
30 enums::{BookType, OrderSide},
31 identifiers::InstrumentId,
32 instruments::{Instrument, InstrumentAny},
33 orderbook::OrderBook,
34 orders::{Order, OrderAny},
35 types::{Price, Quantity},
36};
37use rand::{RngExt, SeedableRng, rngs::StdRng};
38use rust_decimal::Decimal;
39use rust_decimal_macros::dec;
40
41const UNLIMITED_LIQUIDITY_UNITS: u64 = 10_000_000_000;
43
44fn unlimited_liquidity(precision: u8) -> Quantity {
45 Quantity::from_mantissa_exponent(UNLIMITED_LIQUIDITY_UNITS, 0, precision)
46}
47
48pub trait FillModel {
49 fn is_limit_filled(&mut self) -> anyhow::Result<bool>;
55
56 fn is_slipped(&mut self) -> anyhow::Result<bool>;
62
63 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
73 Ok(false)
74 }
75
76 fn get_orderbook_for_fill_simulation(
88 &mut self,
89 instrument: &InstrumentAny,
90 order: &OrderAny,
91 best_bid: Price,
92 best_ask: Price,
93 ) -> anyhow::Result<Option<OrderBook>>;
94}
95
96#[derive(Clone)]
98pub struct FillModelHandle(Rc<RefCell<dyn FillModel>>);
99
100impl FillModelHandle {
101 #[must_use]
103 pub fn new<T>(model: T) -> Self
104 where
105 T: FillModel + 'static,
106 {
107 Self(Rc::new(RefCell::new(model)))
108 }
109
110 #[must_use]
112 pub fn from_rc(model: Rc<RefCell<dyn FillModel>>) -> Self {
113 Self(model)
114 }
115}
116
117impl Debug for FillModelHandle {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_tuple(stringify!(FillModelHandle))
120 .field(&"<dyn FillModel>")
121 .finish()
122 }
123}
124
125impl FillModel for FillModelHandle {
126 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
127 self.0.borrow_mut().is_limit_filled()
128 }
129
130 fn is_slipped(&mut self) -> anyhow::Result<bool> {
131 self.0.borrow_mut().is_slipped()
132 }
133
134 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
135 self.0.borrow().fill_limit_inside_spread()
136 }
137
138 fn get_orderbook_for_fill_simulation(
139 &mut self,
140 instrument: &InstrumentAny,
141 order: &OrderAny,
142 best_bid: Price,
143 best_ask: Price,
144 ) -> anyhow::Result<Option<OrderBook>> {
145 self.0
146 .borrow_mut()
147 .get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
148 }
149}
150
151impl Default for FillModelHandle {
152 fn default() -> Self {
153 FillModelAny::default().into()
154 }
155}
156
157impl From<FillModelAny> for FillModelHandle {
158 fn from(model: FillModelAny) -> Self {
159 Self::new(model)
160 }
161}
162
163#[derive(Debug)]
164pub struct ProbabilisticFillState {
165 prob_fill_on_limit: f64,
166 prob_slippage: f64,
167 random_seed: Option<u64>,
168 rng: StdRng,
169}
170
171impl ProbabilisticFillState {
172 pub fn new(
178 prob_fill_on_limit: f64,
179 prob_slippage: f64,
180 random_seed: Option<u64>,
181 ) -> anyhow::Result<Self> {
182 check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")?;
183 check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage")?;
184 let rng = match random_seed {
185 Some(seed) => StdRng::seed_from_u64(seed),
186 None => default_std_rng(),
187 };
188 Ok(Self {
189 prob_fill_on_limit,
190 prob_slippage,
191 random_seed,
192 rng,
193 })
194 }
195
196 pub fn is_limit_filled(&mut self) -> bool {
197 self.event_success(self.prob_fill_on_limit)
198 }
199
200 pub fn is_slipped(&mut self) -> bool {
201 self.event_success(self.prob_slippage)
202 }
203
204 pub fn random_bool(&mut self, probability: f64) -> bool {
205 self.event_success(probability)
206 }
207
208 fn event_success(&mut self, probability: f64) -> bool {
209 match probability {
210 0.0 => false,
211 1.0 => true,
212 _ => self.rng.random_bool(probability),
213 }
214 }
215}
216
217impl Clone for ProbabilisticFillState {
218 fn clone(&self) -> Self {
219 Self::new(
220 self.prob_fill_on_limit,
221 self.prob_slippage,
222 self.random_seed,
223 )
224 .expect("ProbabilisticFillState clone should not fail with valid parameters")
225 }
226}
227
228fn default_std_rng() -> StdRng {
229 #[cfg(all(feature = "simulation", madsim))]
230 {
231 if madsim::runtime::Handle::try_current().is_ok() {
236 let mut seed = [0u8; 32];
237 madsim::rand::thread_rng().fill_bytes(&mut seed);
238 return StdRng::from_seed(seed);
239 }
240 }
241
242 StdRng::from_rng(&mut rand::rng()) }
244
245fn build_l2_book(instrument_id: InstrumentId) -> OrderBook {
246 OrderBook::new(instrument_id, BookType::L2_MBP)
247}
248
249fn add_order(book: &mut OrderBook, side: OrderSide, price: Price, size: Quantity, order_id: u64) {
250 let order = BookOrder::new(side, price, size, order_id);
251 book.add(order, 0, 0, UnixNanos::default());
252}
253
254#[derive(Debug)]
255#[cfg_attr(
256 feature = "python",
257 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
258)]
259#[cfg_attr(
260 feature = "python",
261 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
262)]
263pub struct DefaultFillModel {
264 state: ProbabilisticFillState,
265}
266
267impl DefaultFillModel {
268 pub fn new(
274 prob_fill_on_limit: f64,
275 prob_slippage: f64,
276 random_seed: Option<u64>,
277 ) -> anyhow::Result<Self> {
278 Ok(Self {
279 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
280 })
281 }
282}
283
284impl Clone for DefaultFillModel {
285 fn clone(&self) -> Self {
286 Self {
287 state: self.state.clone(),
288 }
289 }
290}
291
292impl Default for DefaultFillModel {
293 fn default() -> Self {
294 Self::new(1.0, 0.0, None).unwrap()
295 }
296}
297
298impl Display for DefaultFillModel {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 write!(
301 f,
302 "DefaultFillModel(prob_fill_on_limit: {}, prob_slippage: {})",
303 self.state.prob_fill_on_limit, self.state.prob_slippage
304 )
305 }
306}
307
308impl FillModel for DefaultFillModel {
309 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
310 Ok(self.state.is_limit_filled())
311 }
312
313 fn is_slipped(&mut self) -> anyhow::Result<bool> {
314 Ok(self.state.is_slipped())
315 }
316
317 fn get_orderbook_for_fill_simulation(
318 &mut self,
319 _instrument: &InstrumentAny,
320 _order: &OrderAny,
321 _best_bid: Price,
322 _best_ask: Price,
323 ) -> anyhow::Result<Option<OrderBook>> {
324 Ok(None)
325 }
326}
327
328#[derive(Debug)]
330#[cfg_attr(
331 feature = "python",
332 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
333)]
334#[cfg_attr(
335 feature = "python",
336 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
337)]
338pub struct BestPriceFillModel {
339 state: ProbabilisticFillState,
340}
341
342impl BestPriceFillModel {
343 pub fn new(
349 prob_fill_on_limit: f64,
350 prob_slippage: f64,
351 random_seed: Option<u64>,
352 ) -> anyhow::Result<Self> {
353 Ok(Self {
354 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
355 })
356 }
357}
358
359impl Clone for BestPriceFillModel {
360 fn clone(&self) -> Self {
361 Self {
362 state: self.state.clone(),
363 }
364 }
365}
366
367impl Default for BestPriceFillModel {
368 fn default() -> Self {
369 Self::new(1.0, 0.0, None).unwrap()
370 }
371}
372
373impl FillModel for BestPriceFillModel {
374 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
375 Ok(self.state.is_limit_filled())
376 }
377
378 fn is_slipped(&mut self) -> anyhow::Result<bool> {
379 Ok(self.state.is_slipped())
380 }
381
382 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
383 Ok(true)
384 }
385
386 fn get_orderbook_for_fill_simulation(
387 &mut self,
388 instrument: &InstrumentAny,
389 _order: &OrderAny,
390 best_bid: Price,
391 best_ask: Price,
392 ) -> anyhow::Result<Option<OrderBook>> {
393 let mut book = build_l2_book(instrument.id());
394 let size_prec = instrument.size_precision();
395 add_order(
396 &mut book,
397 OrderSide::Buy,
398 best_bid,
399 unlimited_liquidity(size_prec),
400 1,
401 );
402 add_order(
403 &mut book,
404 OrderSide::Sell,
405 best_ask,
406 unlimited_liquidity(size_prec),
407 2,
408 );
409 Ok(Some(book))
410 }
411}
412
413#[derive(Debug)]
415#[cfg_attr(
416 feature = "python",
417 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
418)]
419#[cfg_attr(
420 feature = "python",
421 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
422)]
423pub struct OneTickSlippageFillModel {
424 state: ProbabilisticFillState,
425}
426
427impl OneTickSlippageFillModel {
428 pub fn new(
434 prob_fill_on_limit: f64,
435 prob_slippage: f64,
436 random_seed: Option<u64>,
437 ) -> anyhow::Result<Self> {
438 Ok(Self {
439 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
440 })
441 }
442}
443
444impl Clone for OneTickSlippageFillModel {
445 fn clone(&self) -> Self {
446 Self {
447 state: self.state.clone(),
448 }
449 }
450}
451
452impl Default for OneTickSlippageFillModel {
453 fn default() -> Self {
454 Self::new(1.0, 0.0, None).unwrap()
455 }
456}
457
458impl FillModel for OneTickSlippageFillModel {
459 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
460 Ok(self.state.is_limit_filled())
461 }
462
463 fn is_slipped(&mut self) -> anyhow::Result<bool> {
464 Ok(self.state.is_slipped())
465 }
466
467 fn get_orderbook_for_fill_simulation(
468 &mut self,
469 instrument: &InstrumentAny,
470 _order: &OrderAny,
471 best_bid: Price,
472 best_ask: Price,
473 ) -> anyhow::Result<Option<OrderBook>> {
474 let tick = instrument.price_increment();
475 let size_prec = instrument.size_precision();
476 let mut book = build_l2_book(instrument.id());
477
478 add_order(
479 &mut book,
480 OrderSide::Buy,
481 best_bid - tick,
482 unlimited_liquidity(size_prec),
483 1,
484 );
485 add_order(
486 &mut book,
487 OrderSide::Sell,
488 best_ask + tick,
489 unlimited_liquidity(size_prec),
490 2,
491 );
492 Ok(Some(book))
493 }
494}
495
496#[derive(Debug)]
498#[cfg_attr(
499 feature = "python",
500 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
501)]
502#[cfg_attr(
503 feature = "python",
504 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
505)]
506pub struct ProbabilisticFillModel {
507 state: ProbabilisticFillState,
508}
509
510impl ProbabilisticFillModel {
511 pub fn new(
517 prob_fill_on_limit: f64,
518 prob_slippage: f64,
519 random_seed: Option<u64>,
520 ) -> anyhow::Result<Self> {
521 Ok(Self {
522 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
523 })
524 }
525}
526
527impl Clone for ProbabilisticFillModel {
528 fn clone(&self) -> Self {
529 Self {
530 state: self.state.clone(),
531 }
532 }
533}
534
535impl Default for ProbabilisticFillModel {
536 fn default() -> Self {
537 Self::new(1.0, 0.0, None).unwrap()
538 }
539}
540
541impl FillModel for ProbabilisticFillModel {
542 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
543 Ok(self.state.is_limit_filled())
544 }
545
546 fn is_slipped(&mut self) -> anyhow::Result<bool> {
547 Ok(self.state.is_slipped())
548 }
549
550 fn get_orderbook_for_fill_simulation(
551 &mut self,
552 instrument: &InstrumentAny,
553 _order: &OrderAny,
554 best_bid: Price,
555 best_ask: Price,
556 ) -> anyhow::Result<Option<OrderBook>> {
557 let tick = instrument.price_increment();
558 let size_prec = instrument.size_precision();
559 let mut book = build_l2_book(instrument.id());
560
561 if self.state.random_bool(0.5) {
562 add_order(
563 &mut book,
564 OrderSide::Buy,
565 best_bid,
566 unlimited_liquidity(size_prec),
567 1,
568 );
569 add_order(
570 &mut book,
571 OrderSide::Sell,
572 best_ask,
573 unlimited_liquidity(size_prec),
574 2,
575 );
576 } else {
577 add_order(
578 &mut book,
579 OrderSide::Buy,
580 best_bid - tick,
581 unlimited_liquidity(size_prec),
582 1,
583 );
584 add_order(
585 &mut book,
586 OrderSide::Sell,
587 best_ask + tick,
588 unlimited_liquidity(size_prec),
589 2,
590 );
591 }
592 Ok(Some(book))
593 }
594}
595
596#[derive(Debug)]
598#[cfg_attr(
599 feature = "python",
600 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
601)]
602#[cfg_attr(
603 feature = "python",
604 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
605)]
606pub struct TwoTierFillModel {
607 state: ProbabilisticFillState,
608}
609
610impl TwoTierFillModel {
611 pub fn new(
617 prob_fill_on_limit: f64,
618 prob_slippage: f64,
619 random_seed: Option<u64>,
620 ) -> anyhow::Result<Self> {
621 Ok(Self {
622 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
623 })
624 }
625}
626
627impl Clone for TwoTierFillModel {
628 fn clone(&self) -> Self {
629 Self {
630 state: self.state.clone(),
631 }
632 }
633}
634
635impl Default for TwoTierFillModel {
636 fn default() -> Self {
637 Self::new(1.0, 0.0, None).unwrap()
638 }
639}
640
641impl FillModel for TwoTierFillModel {
642 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
643 Ok(self.state.is_limit_filled())
644 }
645
646 fn is_slipped(&mut self) -> anyhow::Result<bool> {
647 Ok(self.state.is_slipped())
648 }
649
650 fn get_orderbook_for_fill_simulation(
651 &mut self,
652 instrument: &InstrumentAny,
653 _order: &OrderAny,
654 best_bid: Price,
655 best_ask: Price,
656 ) -> anyhow::Result<Option<OrderBook>> {
657 let tick = instrument.price_increment();
658 let size_prec = instrument.size_precision();
659 let mut book = build_l2_book(instrument.id());
660
661 add_order(
662 &mut book,
663 OrderSide::Buy,
664 best_bid,
665 Quantity::new(10.0, size_prec),
666 1,
667 );
668 add_order(
669 &mut book,
670 OrderSide::Sell,
671 best_ask,
672 Quantity::new(10.0, size_prec),
673 2,
674 );
675 add_order(
676 &mut book,
677 OrderSide::Buy,
678 best_bid - tick,
679 unlimited_liquidity(size_prec),
680 3,
681 );
682 add_order(
683 &mut book,
684 OrderSide::Sell,
685 best_ask + tick,
686 unlimited_liquidity(size_prec),
687 4,
688 );
689 Ok(Some(book))
690 }
691}
692
693#[derive(Debug)]
695#[cfg_attr(
696 feature = "python",
697 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
698)]
699#[cfg_attr(
700 feature = "python",
701 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
702)]
703pub struct ThreeTierFillModel {
704 state: ProbabilisticFillState,
705}
706
707impl ThreeTierFillModel {
708 pub fn new(
714 prob_fill_on_limit: f64,
715 prob_slippage: f64,
716 random_seed: Option<u64>,
717 ) -> anyhow::Result<Self> {
718 Ok(Self {
719 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
720 })
721 }
722}
723
724impl Clone for ThreeTierFillModel {
725 fn clone(&self) -> Self {
726 Self {
727 state: self.state.clone(),
728 }
729 }
730}
731
732impl Default for ThreeTierFillModel {
733 fn default() -> Self {
734 Self::new(1.0, 0.0, None).unwrap()
735 }
736}
737
738impl FillModel for ThreeTierFillModel {
739 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
740 Ok(self.state.is_limit_filled())
741 }
742
743 fn is_slipped(&mut self) -> anyhow::Result<bool> {
744 Ok(self.state.is_slipped())
745 }
746
747 fn get_orderbook_for_fill_simulation(
748 &mut self,
749 instrument: &InstrumentAny,
750 _order: &OrderAny,
751 best_bid: Price,
752 best_ask: Price,
753 ) -> anyhow::Result<Option<OrderBook>> {
754 let tick = instrument.price_increment();
755 let two_ticks = tick + tick;
756 let size_prec = instrument.size_precision();
757 let mut book = build_l2_book(instrument.id());
758
759 add_order(
760 &mut book,
761 OrderSide::Buy,
762 best_bid,
763 Quantity::new(50.0, size_prec),
764 1,
765 );
766 add_order(
767 &mut book,
768 OrderSide::Sell,
769 best_ask,
770 Quantity::new(50.0, size_prec),
771 2,
772 );
773 add_order(
774 &mut book,
775 OrderSide::Buy,
776 best_bid - tick,
777 Quantity::new(30.0, size_prec),
778 3,
779 );
780 add_order(
781 &mut book,
782 OrderSide::Sell,
783 best_ask + tick,
784 Quantity::new(30.0, size_prec),
785 4,
786 );
787 add_order(
788 &mut book,
789 OrderSide::Buy,
790 best_bid - two_ticks,
791 Quantity::new(20.0, size_prec),
792 5,
793 );
794 add_order(
795 &mut book,
796 OrderSide::Sell,
797 best_ask + two_ticks,
798 Quantity::new(20.0, size_prec),
799 6,
800 );
801 Ok(Some(book))
802 }
803}
804
805#[derive(Debug)]
807#[cfg_attr(
808 feature = "python",
809 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
810)]
811#[cfg_attr(
812 feature = "python",
813 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
814)]
815pub struct LimitOrderPartialFillModel {
816 state: ProbabilisticFillState,
817}
818
819impl LimitOrderPartialFillModel {
820 pub fn new(
826 prob_fill_on_limit: f64,
827 prob_slippage: f64,
828 random_seed: Option<u64>,
829 ) -> anyhow::Result<Self> {
830 Ok(Self {
831 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
832 })
833 }
834}
835
836impl Clone for LimitOrderPartialFillModel {
837 fn clone(&self) -> Self {
838 Self {
839 state: self.state.clone(),
840 }
841 }
842}
843
844impl Default for LimitOrderPartialFillModel {
845 fn default() -> Self {
846 Self::new(1.0, 0.0, None).unwrap()
847 }
848}
849
850impl FillModel for LimitOrderPartialFillModel {
851 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
852 Ok(self.state.is_limit_filled())
853 }
854
855 fn is_slipped(&mut self) -> anyhow::Result<bool> {
856 Ok(self.state.is_slipped())
857 }
858
859 fn get_orderbook_for_fill_simulation(
860 &mut self,
861 instrument: &InstrumentAny,
862 _order: &OrderAny,
863 best_bid: Price,
864 best_ask: Price,
865 ) -> anyhow::Result<Option<OrderBook>> {
866 let tick = instrument.price_increment();
867 let size_prec = instrument.size_precision();
868 let mut book = build_l2_book(instrument.id());
869
870 add_order(
871 &mut book,
872 OrderSide::Buy,
873 best_bid,
874 Quantity::new(5.0, size_prec),
875 1,
876 );
877 add_order(
878 &mut book,
879 OrderSide::Sell,
880 best_ask,
881 Quantity::new(5.0, size_prec),
882 2,
883 );
884 add_order(
885 &mut book,
886 OrderSide::Buy,
887 best_bid - tick,
888 unlimited_liquidity(size_prec),
889 3,
890 );
891 add_order(
892 &mut book,
893 OrderSide::Sell,
894 best_ask + tick,
895 unlimited_liquidity(size_prec),
896 4,
897 );
898 Ok(Some(book))
899 }
900}
901
902#[derive(Debug)]
905#[cfg_attr(
906 feature = "python",
907 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
908)]
909#[cfg_attr(
910 feature = "python",
911 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
912)]
913pub struct SizeAwareFillModel {
914 state: ProbabilisticFillState,
915}
916
917impl SizeAwareFillModel {
918 pub fn new(
924 prob_fill_on_limit: f64,
925 prob_slippage: f64,
926 random_seed: Option<u64>,
927 ) -> anyhow::Result<Self> {
928 Ok(Self {
929 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
930 })
931 }
932}
933
934impl Clone for SizeAwareFillModel {
935 fn clone(&self) -> Self {
936 Self {
937 state: self.state.clone(),
938 }
939 }
940}
941
942impl Default for SizeAwareFillModel {
943 fn default() -> Self {
944 Self::new(1.0, 0.0, None).unwrap()
945 }
946}
947
948impl FillModel for SizeAwareFillModel {
949 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
950 Ok(self.state.is_limit_filled())
951 }
952
953 fn is_slipped(&mut self) -> anyhow::Result<bool> {
954 Ok(self.state.is_slipped())
955 }
956
957 fn get_orderbook_for_fill_simulation(
958 &mut self,
959 instrument: &InstrumentAny,
960 order: &OrderAny,
961 best_bid: Price,
962 best_ask: Price,
963 ) -> anyhow::Result<Option<OrderBook>> {
964 let tick = instrument.price_increment();
965 let size_prec = instrument.size_precision();
966 let mut book = build_l2_book(instrument.id());
967
968 let threshold = Quantity::new(10.0, size_prec);
969 if order.quantity() <= threshold {
970 add_order(
972 &mut book,
973 OrderSide::Buy,
974 best_bid,
975 Quantity::new(50.0, size_prec),
976 1,
977 );
978 add_order(
979 &mut book,
980 OrderSide::Sell,
981 best_ask,
982 Quantity::new(50.0, size_prec),
983 2,
984 );
985 } else {
986 let remaining = order.quantity() - threshold;
988 add_order(&mut book, OrderSide::Buy, best_bid, threshold, 1);
989 add_order(&mut book, OrderSide::Sell, best_ask, threshold, 2);
990 add_order(&mut book, OrderSide::Buy, best_bid - tick, remaining, 3);
991 add_order(&mut book, OrderSide::Sell, best_ask + tick, remaining, 4);
992 }
993 Ok(Some(book))
994 }
995}
996
997#[derive(Debug)]
999#[cfg_attr(
1000 feature = "python",
1001 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
1002)]
1003#[cfg_attr(
1004 feature = "python",
1005 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1006)]
1007pub struct CompetitionAwareFillModel {
1008 state: ProbabilisticFillState,
1009 liquidity_factor: Decimal,
1010}
1011
1012impl CompetitionAwareFillModel {
1013 pub fn new(
1019 prob_fill_on_limit: f64,
1020 prob_slippage: f64,
1021 random_seed: Option<u64>,
1022 liquidity_factor: f64,
1023 ) -> anyhow::Result<Self> {
1024 let state = ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?;
1025 check_in_range_inclusive_f64(liquidity_factor, 0.0, 1.0, "liquidity_factor")?;
1026 let liquidity_factor = Decimal::try_from(liquidity_factor)?;
1027
1028 Ok(Self {
1029 state,
1030 liquidity_factor,
1031 })
1032 }
1033}
1034
1035impl Clone for CompetitionAwareFillModel {
1036 fn clone(&self) -> Self {
1037 Self {
1038 state: self.state.clone(),
1039 liquidity_factor: self.liquidity_factor,
1040 }
1041 }
1042}
1043
1044impl Default for CompetitionAwareFillModel {
1045 fn default() -> Self {
1046 Self::new(1.0, 0.0, None, 0.3).unwrap()
1047 }
1048}
1049
1050impl FillModel for CompetitionAwareFillModel {
1051 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1052 Ok(self.state.is_limit_filled())
1053 }
1054
1055 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1056 Ok(self.state.is_slipped())
1057 }
1058
1059 fn get_orderbook_for_fill_simulation(
1060 &mut self,
1061 instrument: &InstrumentAny,
1062 _order: &OrderAny,
1063 best_bid: Price,
1064 best_ask: Price,
1065 ) -> anyhow::Result<Option<OrderBook>> {
1066 let size_prec = instrument.size_precision();
1067 let mut book = build_l2_book(instrument.id());
1068
1069 let available = Quantity::from_decimal_dp(
1071 (dec!(1000) * self.liquidity_factor).max(Decimal::ONE),
1072 size_prec,
1073 )?;
1074
1075 add_order(&mut book, OrderSide::Buy, best_bid, available, 1);
1076 add_order(&mut book, OrderSide::Sell, best_ask, available, 2);
1077 Ok(Some(book))
1078 }
1079}
1080
1081#[derive(Debug)]
1084#[cfg_attr(
1085 feature = "python",
1086 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
1087)]
1088#[cfg_attr(
1089 feature = "python",
1090 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1091)]
1092pub struct VolumeSensitiveFillModel {
1093 state: ProbabilisticFillState,
1094 recent_volume: f64,
1095}
1096
1097impl VolumeSensitiveFillModel {
1098 pub fn new(
1104 prob_fill_on_limit: f64,
1105 prob_slippage: f64,
1106 random_seed: Option<u64>,
1107 ) -> anyhow::Result<Self> {
1108 Ok(Self {
1109 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1110 recent_volume: 1000.0,
1111 })
1112 }
1113
1114 pub fn set_recent_volume(&mut self, volume: f64) {
1115 self.recent_volume = volume;
1116 }
1117}
1118
1119impl Clone for VolumeSensitiveFillModel {
1120 fn clone(&self) -> Self {
1121 Self {
1122 state: self.state.clone(),
1123 recent_volume: self.recent_volume,
1124 }
1125 }
1126}
1127
1128impl Default for VolumeSensitiveFillModel {
1129 fn default() -> Self {
1130 Self::new(1.0, 0.0, None).unwrap()
1131 }
1132}
1133
1134impl FillModel for VolumeSensitiveFillModel {
1135 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1136 Ok(self.state.is_limit_filled())
1137 }
1138
1139 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1140 Ok(self.state.is_slipped())
1141 }
1142
1143 fn get_orderbook_for_fill_simulation(
1144 &mut self,
1145 instrument: &InstrumentAny,
1146 _order: &OrderAny,
1147 best_bid: Price,
1148 best_ask: Price,
1149 ) -> anyhow::Result<Option<OrderBook>> {
1150 let tick = instrument.price_increment();
1151 let size_prec = instrument.size_precision();
1152 let mut book = build_l2_book(instrument.id());
1153
1154 check_non_negative_f64(self.recent_volume, "recent_volume")?;
1155 let recent_volume = Decimal::try_from(self.recent_volume)?;
1156
1157 let available =
1159 Quantity::from_decimal_dp((recent_volume * dec!(0.25)).max(Decimal::ONE), size_prec)?;
1160
1161 add_order(&mut book, OrderSide::Buy, best_bid, available, 1);
1162 add_order(&mut book, OrderSide::Sell, best_ask, available, 2);
1163 add_order(
1164 &mut book,
1165 OrderSide::Buy,
1166 best_bid - tick,
1167 unlimited_liquidity(size_prec),
1168 3,
1169 );
1170 add_order(
1171 &mut book,
1172 OrderSide::Sell,
1173 best_ask + tick,
1174 unlimited_liquidity(size_prec),
1175 4,
1176 );
1177 Ok(Some(book))
1178 }
1179}
1180
1181#[derive(Debug)]
1184#[cfg_attr(
1185 feature = "python",
1186 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
1187)]
1188#[cfg_attr(
1189 feature = "python",
1190 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1191)]
1192pub struct MarketHoursFillModel {
1193 state: ProbabilisticFillState,
1194 is_low_liquidity: bool,
1195}
1196
1197impl MarketHoursFillModel {
1198 pub fn new(
1204 prob_fill_on_limit: f64,
1205 prob_slippage: f64,
1206 random_seed: Option<u64>,
1207 ) -> anyhow::Result<Self> {
1208 Ok(Self {
1209 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1210 is_low_liquidity: false,
1211 })
1212 }
1213
1214 pub fn set_low_liquidity_period(&mut self, is_low_liquidity: bool) {
1215 self.is_low_liquidity = is_low_liquidity;
1216 }
1217
1218 pub fn is_low_liquidity_period(&self) -> bool {
1219 self.is_low_liquidity
1220 }
1221}
1222
1223impl Clone for MarketHoursFillModel {
1224 fn clone(&self) -> Self {
1225 Self {
1226 state: self.state.clone(),
1227 is_low_liquidity: self.is_low_liquidity,
1228 }
1229 }
1230}
1231
1232impl Default for MarketHoursFillModel {
1233 fn default() -> Self {
1234 Self::new(1.0, 0.0, None).unwrap()
1235 }
1236}
1237
1238impl FillModel for MarketHoursFillModel {
1239 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1240 Ok(self.state.is_limit_filled())
1241 }
1242
1243 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1244 Ok(self.state.is_slipped())
1245 }
1246
1247 fn get_orderbook_for_fill_simulation(
1248 &mut self,
1249 instrument: &InstrumentAny,
1250 _order: &OrderAny,
1251 best_bid: Price,
1252 best_ask: Price,
1253 ) -> anyhow::Result<Option<OrderBook>> {
1254 let tick = instrument.price_increment();
1255 let size_prec = instrument.size_precision();
1256 let mut book = build_l2_book(instrument.id());
1257 let normal_volume = 500.0;
1258
1259 if self.is_low_liquidity {
1260 add_order(
1261 &mut book,
1262 OrderSide::Buy,
1263 best_bid - tick,
1264 Quantity::new(normal_volume, size_prec),
1265 1,
1266 );
1267 add_order(
1268 &mut book,
1269 OrderSide::Sell,
1270 best_ask + tick,
1271 Quantity::new(normal_volume, size_prec),
1272 2,
1273 );
1274 } else {
1275 add_order(
1276 &mut book,
1277 OrderSide::Buy,
1278 best_bid,
1279 Quantity::new(normal_volume, size_prec),
1280 1,
1281 );
1282 add_order(
1283 &mut book,
1284 OrderSide::Sell,
1285 best_ask,
1286 Quantity::new(normal_volume, size_prec),
1287 2,
1288 );
1289 }
1290 Ok(Some(book))
1291 }
1292}
1293
1294#[derive(Clone, Debug)]
1295pub enum FillModelAny {
1296 Default(DefaultFillModel),
1297 BestPrice(BestPriceFillModel),
1298 OneTickSlippage(OneTickSlippageFillModel),
1299 Probabilistic(ProbabilisticFillModel),
1300 TwoTier(TwoTierFillModel),
1301 ThreeTier(ThreeTierFillModel),
1302 LimitOrderPartialFill(LimitOrderPartialFillModel),
1303 SizeAware(SizeAwareFillModel),
1304 CompetitionAware(CompetitionAwareFillModel),
1305 VolumeSensitive(VolumeSensitiveFillModel),
1306 MarketHours(MarketHoursFillModel),
1307}
1308
1309impl FillModel for FillModelAny {
1310 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1311 match self {
1312 Self::Default(m) => m.is_limit_filled(),
1313 Self::BestPrice(m) => m.is_limit_filled(),
1314 Self::OneTickSlippage(m) => m.is_limit_filled(),
1315 Self::Probabilistic(m) => m.is_limit_filled(),
1316 Self::TwoTier(m) => m.is_limit_filled(),
1317 Self::ThreeTier(m) => m.is_limit_filled(),
1318 Self::LimitOrderPartialFill(m) => m.is_limit_filled(),
1319 Self::SizeAware(m) => m.is_limit_filled(),
1320 Self::CompetitionAware(m) => m.is_limit_filled(),
1321 Self::VolumeSensitive(m) => m.is_limit_filled(),
1322 Self::MarketHours(m) => m.is_limit_filled(),
1323 }
1324 }
1325
1326 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
1327 match self {
1328 Self::Default(m) => m.fill_limit_inside_spread(),
1329 Self::BestPrice(m) => m.fill_limit_inside_spread(),
1330 Self::OneTickSlippage(m) => m.fill_limit_inside_spread(),
1331 Self::Probabilistic(m) => m.fill_limit_inside_spread(),
1332 Self::TwoTier(m) => m.fill_limit_inside_spread(),
1333 Self::ThreeTier(m) => m.fill_limit_inside_spread(),
1334 Self::LimitOrderPartialFill(m) => m.fill_limit_inside_spread(),
1335 Self::SizeAware(m) => m.fill_limit_inside_spread(),
1336 Self::CompetitionAware(m) => m.fill_limit_inside_spread(),
1337 Self::VolumeSensitive(m) => m.fill_limit_inside_spread(),
1338 Self::MarketHours(m) => m.fill_limit_inside_spread(),
1339 }
1340 }
1341
1342 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1343 match self {
1344 Self::Default(m) => m.is_slipped(),
1345 Self::BestPrice(m) => m.is_slipped(),
1346 Self::OneTickSlippage(m) => m.is_slipped(),
1347 Self::Probabilistic(m) => m.is_slipped(),
1348 Self::TwoTier(m) => m.is_slipped(),
1349 Self::ThreeTier(m) => m.is_slipped(),
1350 Self::LimitOrderPartialFill(m) => m.is_slipped(),
1351 Self::SizeAware(m) => m.is_slipped(),
1352 Self::CompetitionAware(m) => m.is_slipped(),
1353 Self::VolumeSensitive(m) => m.is_slipped(),
1354 Self::MarketHours(m) => m.is_slipped(),
1355 }
1356 }
1357
1358 fn get_orderbook_for_fill_simulation(
1359 &mut self,
1360 instrument: &InstrumentAny,
1361 order: &OrderAny,
1362 best_bid: Price,
1363 best_ask: Price,
1364 ) -> anyhow::Result<Option<OrderBook>> {
1365 match self {
1366 Self::Default(m) => {
1367 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1368 }
1369 Self::BestPrice(m) => {
1370 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1371 }
1372 Self::OneTickSlippage(m) => {
1373 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1374 }
1375 Self::Probabilistic(m) => {
1376 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1377 }
1378 Self::TwoTier(m) => {
1379 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1380 }
1381 Self::ThreeTier(m) => {
1382 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1383 }
1384 Self::LimitOrderPartialFill(m) => {
1385 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1386 }
1387 Self::SizeAware(m) => {
1388 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1389 }
1390 Self::CompetitionAware(m) => {
1391 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1392 }
1393 Self::VolumeSensitive(m) => {
1394 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1395 }
1396 Self::MarketHours(m) => {
1397 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1398 }
1399 }
1400 }
1401}
1402
1403impl Default for FillModelAny {
1404 fn default() -> Self {
1405 Self::Default(DefaultFillModel::default())
1406 }
1407}
1408
1409impl Display for FillModelAny {
1410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1411 match self {
1412 Self::Default(m) => write!(f, "{m}"),
1413 Self::BestPrice(_) => write!(f, "BestPriceFillModel"),
1414 Self::OneTickSlippage(_) => write!(f, "OneTickSlippageFillModel"),
1415 Self::Probabilistic(_) => write!(f, "ProbabilisticFillModel"),
1416 Self::TwoTier(_) => write!(f, "TwoTierFillModel"),
1417 Self::ThreeTier(_) => write!(f, "ThreeTierFillModel"),
1418 Self::LimitOrderPartialFill(_) => write!(f, "LimitOrderPartialFillModel"),
1419 Self::SizeAware(_) => write!(f, "SizeAwareFillModel"),
1420 Self::CompetitionAware(_) => write!(f, "CompetitionAwareFillModel"),
1421 Self::VolumeSensitive(_) => write!(f, "VolumeSensitiveFillModel"),
1422 Self::MarketHours(_) => write!(f, "MarketHoursFillModel"),
1423 }
1424 }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429 use nautilus_core::correctness::CorrectnessError;
1430 use nautilus_model::{
1431 enums::OrderType,
1432 instruments::stubs::{audusd_sim, crypto_perpetual_ethusdt},
1433 orders::builder::OrderTestBuilder,
1434 };
1435 use rstest::{fixture, rstest};
1436
1437 use super::*;
1438
1439 #[fixture]
1440 fn fill_model() -> DefaultFillModel {
1441 let seed = 42;
1442 DefaultFillModel::new(0.5, 0.1, Some(seed)).unwrap()
1443 }
1444
1445 #[rstest]
1446 fn test_fill_model_param_prob_fill_on_limit_error() {
1447 let error = DefaultFillModel::new(1.1, 0.1, None).unwrap_err();
1448
1449 assert_eq!(
1450 error.downcast_ref::<CorrectnessError>(),
1451 Some(&CorrectnessError::OutOfRange {
1452 param: "prob_fill_on_limit".to_string(),
1453 min: "0".to_string(),
1454 max: "1".to_string(),
1455 value: "1.1".to_string(),
1456 type_name: "f64",
1457 })
1458 );
1459 assert_eq!(
1460 error.to_string(),
1461 "invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
1462 );
1463 }
1464
1465 #[rstest]
1466 fn test_fill_model_param_prob_slippage_error() {
1467 let error = DefaultFillModel::new(0.5, 1.1, None).unwrap_err();
1468
1469 assert_eq!(
1470 error.downcast_ref::<CorrectnessError>(),
1471 Some(&CorrectnessError::OutOfRange {
1472 param: "prob_slippage".to_string(),
1473 min: "0".to_string(),
1474 max: "1".to_string(),
1475 value: "1.1".to_string(),
1476 type_name: "f64",
1477 })
1478 );
1479 assert_eq!(
1480 error.to_string(),
1481 "invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
1482 );
1483 }
1484
1485 #[rstest]
1486 #[case(f64::NAN, "NaN")]
1487 #[case(f64::INFINITY, "inf")]
1488 #[case(f64::NEG_INFINITY, "-inf")]
1489 fn test_competition_aware_fill_model_rejects_non_finite_liquidity_factor(
1490 #[case] value: f64,
1491 #[case] expected_value: &str,
1492 ) {
1493 let error = CompetitionAwareFillModel::new(1.0, 0.0, None, value).unwrap_err();
1494
1495 assert_eq!(
1496 error.downcast_ref::<CorrectnessError>(),
1497 Some(&CorrectnessError::InvalidValue {
1498 param: "liquidity_factor".to_string(),
1499 value: expected_value.to_string(),
1500 type_name: "f64",
1501 })
1502 );
1503 }
1504
1505 #[rstest]
1506 #[case(-0.1, "-0.1")]
1507 #[case(1.1, "1.1")]
1508 fn test_competition_aware_fill_model_rejects_out_of_range_liquidity_factor(
1509 #[case] value: f64,
1510 #[case] expected_value: &str,
1511 ) {
1512 let error = CompetitionAwareFillModel::new(1.0, 0.0, None, value).unwrap_err();
1513
1514 assert_eq!(
1515 error.downcast_ref::<CorrectnessError>(),
1516 Some(&CorrectnessError::OutOfRange {
1517 param: "liquidity_factor".to_string(),
1518 min: "0".to_string(),
1519 max: "1".to_string(),
1520 value: expected_value.to_string(),
1521 type_name: "f64",
1522 })
1523 );
1524 }
1525
1526 #[rstest]
1527 #[case(f64::NAN, "NaN")]
1528 #[case(f64::INFINITY, "inf")]
1529 #[case(f64::NEG_INFINITY, "-inf")]
1530 fn test_volume_sensitive_fill_model_rejects_non_finite_volume(
1531 #[case] volume: f64,
1532 #[case] expected_value: &str,
1533 ) {
1534 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1535 let order = OrderTestBuilder::new(OrderType::Market)
1536 .instrument_id(instrument.id())
1537 .side(OrderSide::Buy)
1538 .quantity(Quantity::from(100_000))
1539 .build();
1540 let mut model = VolumeSensitiveFillModel::default();
1541 model.set_recent_volume(volume);
1542
1543 let error = model
1544 .get_orderbook_for_fill_simulation(
1545 &instrument,
1546 &order,
1547 Price::from("0.80000"),
1548 Price::from("0.80010"),
1549 )
1550 .unwrap_err();
1551
1552 assert_eq!(
1553 error.downcast_ref::<CorrectnessError>(),
1554 Some(&CorrectnessError::InvalidValue {
1555 param: "recent_volume".to_string(),
1556 value: expected_value.to_string(),
1557 type_name: "f64",
1558 })
1559 );
1560 }
1561
1562 #[rstest]
1563 fn test_volume_sensitive_fill_model_rejects_negative_volume() {
1564 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1565 let order = OrderTestBuilder::new(OrderType::Market)
1566 .instrument_id(instrument.id())
1567 .side(OrderSide::Buy)
1568 .quantity(Quantity::from(100_000))
1569 .build();
1570 let mut model = VolumeSensitiveFillModel::default();
1571 model.set_recent_volume(-1.0);
1572
1573 let error = model
1574 .get_orderbook_for_fill_simulation(
1575 &instrument,
1576 &order,
1577 Price::from("0.80000"),
1578 Price::from("0.80010"),
1579 )
1580 .unwrap_err();
1581
1582 assert_eq!(
1583 error.downcast_ref::<CorrectnessError>(),
1584 Some(&CorrectnessError::NegativeValue {
1585 param: "recent_volume".to_string(),
1586 value: "-1".to_string(),
1587 type_name: "f64",
1588 })
1589 );
1590 }
1591
1592 #[rstest]
1593 fn test_volume_sensitive_fill_model_rejects_volume_above_quantity_range() {
1594 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1595 let order = OrderTestBuilder::new(OrderType::Market)
1596 .instrument_id(instrument.id())
1597 .side(OrderSide::Buy)
1598 .quantity(Quantity::from(100_000))
1599 .build();
1600 let mut model = VolumeSensitiveFillModel::default();
1601 model.set_recent_volume(100_000_000_000_000_000.0);
1602
1603 let error = model
1604 .get_orderbook_for_fill_simulation(
1605 &instrument,
1606 &order,
1607 Price::from("0.80000"),
1608 Price::from("0.80010"),
1609 )
1610 .unwrap_err();
1611
1612 assert!(matches!(
1613 error.downcast_ref::<CorrectnessError>(),
1614 Some(CorrectnessError::PredicateViolation { message })
1615 if message.contains("QuantityRaw") || message.contains("QUANTITY_RAW_MAX")
1616 ));
1617 }
1618
1619 #[rstest]
1620 #[case(0.0, Quantity::from(1))]
1621 #[case(0.5, Quantity::from(500))]
1622 #[case(1.0, Quantity::from(1_000))]
1623 fn test_competition_aware_fill_model_builds_expected_liquidity(
1624 #[case] liquidity_factor: f64,
1625 #[case] expected_size: Quantity,
1626 ) {
1627 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1628 let order = OrderTestBuilder::new(OrderType::Market)
1629 .instrument_id(instrument.id())
1630 .side(OrderSide::Buy)
1631 .quantity(Quantity::from(100_000))
1632 .build();
1633 let best_bid = Price::from("0.80000");
1634 let best_ask = Price::from("0.80010");
1635 let mut model = CompetitionAwareFillModel::new(1.0, 0.0, None, liquidity_factor).unwrap();
1636
1637 let book = model
1638 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1639 .unwrap()
1640 .unwrap();
1641
1642 assert_eq!(book.best_bid_price(), Some(best_bid));
1643 assert_eq!(book.best_ask_price(), Some(best_ask));
1644 assert_eq!(book.best_bid_size(), Some(expected_size));
1645 assert_eq!(book.best_ask_size(), Some(expected_size));
1646 }
1647
1648 #[rstest]
1649 fn test_competition_aware_fill_model_preserves_instrument_size_precision() {
1650 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
1651 let order = OrderTestBuilder::new(OrderType::Market)
1652 .instrument_id(instrument.id())
1653 .side(OrderSide::Buy)
1654 .quantity(Quantity::from(100_000))
1655 .build();
1656 let best_bid = Price::from("2000.00");
1657 let best_ask = Price::from("2000.01");
1658 let mut model = CompetitionAwareFillModel::new(1.0, 0.0, None, 0.001234).unwrap();
1659
1660 let book = model
1661 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1662 .unwrap()
1663 .unwrap();
1664
1665 assert_eq!(book.best_bid_price(), Some(best_bid));
1666 assert_eq!(book.best_ask_price(), Some(best_ask));
1667 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.234")));
1668 assert_eq!(book.best_ask_size(), Some(Quantity::from("1.234")));
1669 }
1670
1671 #[rstest]
1672 fn test_volume_sensitive_fill_model_builds_expected_liquidity() {
1673 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
1674 let order = OrderTestBuilder::new(OrderType::Market)
1675 .instrument_id(instrument.id())
1676 .side(OrderSide::Buy)
1677 .quantity(Quantity::from(100_000))
1678 .build();
1679 let best_bid = Price::from("2000.00");
1680 let best_ask = Price::from("2000.01");
1681 let mut model = VolumeSensitiveFillModel::default();
1682 model.set_recent_volume(5.678);
1683
1684 let book = model
1685 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1686 .unwrap()
1687 .unwrap();
1688
1689 assert_eq!(book.best_bid_price(), Some(best_bid));
1690 assert_eq!(book.best_ask_price(), Some(best_ask));
1691 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.420")));
1692 assert_eq!(book.best_ask_size(), Some(Quantity::from("1.420")));
1693 }
1694
1695 #[rstest]
1696 fn test_fill_model_is_limit_filled(mut fill_model: DefaultFillModel) {
1697 let result = fill_model.is_limit_filled().unwrap();
1699 assert!(!result);
1700 }
1701
1702 #[rstest]
1703 fn test_fill_model_is_slipped(mut fill_model: DefaultFillModel) {
1704 let result = fill_model.is_slipped().unwrap();
1706 assert!(!result);
1707 }
1708
1709 #[rstest]
1710 fn test_default_fill_model_returns_none() {
1711 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1712 let order = OrderTestBuilder::new(OrderType::Market)
1713 .instrument_id(instrument.id())
1714 .side(OrderSide::Buy)
1715 .quantity(Quantity::from(100_000))
1716 .build();
1717
1718 let mut model = DefaultFillModel::default();
1719 let result = model
1720 .get_orderbook_for_fill_simulation(
1721 &instrument,
1722 &order,
1723 Price::from("0.80000"),
1724 Price::from("0.80010"),
1725 )
1726 .unwrap();
1727 assert!(result.is_none());
1728 }
1729
1730 #[rstest]
1731 fn test_best_price_fill_model_returns_book() {
1732 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1733 let order = OrderTestBuilder::new(OrderType::Market)
1734 .instrument_id(instrument.id())
1735 .side(OrderSide::Buy)
1736 .quantity(Quantity::from(100_000))
1737 .build();
1738
1739 let mut model = BestPriceFillModel::default();
1740 let result = model
1741 .get_orderbook_for_fill_simulation(
1742 &instrument,
1743 &order,
1744 Price::from("0.80000"),
1745 Price::from("0.80010"),
1746 )
1747 .unwrap();
1748 assert!(result.is_some());
1749 let book = result.unwrap();
1750 assert_eq!(book.best_bid_price().unwrap(), Price::from("0.80000"));
1751 assert_eq!(book.best_ask_price().unwrap(), Price::from("0.80010"));
1752 }
1753
1754 #[rstest]
1755 fn test_one_tick_slippage_fill_model() {
1756 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1757 let order = OrderTestBuilder::new(OrderType::Market)
1758 .instrument_id(instrument.id())
1759 .side(OrderSide::Buy)
1760 .quantity(Quantity::from(100_000))
1761 .build();
1762
1763 let tick = instrument.price_increment();
1764 let best_bid = Price::from("0.80000");
1765 let best_ask = Price::from("0.80010");
1766
1767 let mut model = OneTickSlippageFillModel::default();
1768 let result = model
1769 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1770 .unwrap();
1771 assert!(result.is_some());
1772 let book = result.unwrap();
1773
1774 assert_eq!(book.best_bid_price().unwrap(), best_bid - tick);
1775 assert_eq!(book.best_ask_price().unwrap(), best_ask + tick);
1776 }
1777
1778 #[rstest]
1779 fn test_fill_model_any_dispatch() {
1780 let model = FillModelAny::default();
1781 assert!(matches!(model, FillModelAny::Default(_)));
1782 }
1783
1784 #[rstest]
1785 fn test_fill_model_any_is_limit_filled() {
1786 let mut model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.1, Some(42)).unwrap());
1787 let result = model.is_limit_filled().unwrap();
1788 assert!(!result);
1789 }
1790
1791 #[rstest]
1792 fn test_fill_model_handle_from_any_owns_state_per_conversion() {
1793 let model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.0, Some(42)).unwrap());
1794 let mut expected_model = model.clone();
1795 let mut first: FillModelHandle = model.clone().into();
1796 let mut second: FillModelHandle = model.into();
1797
1798 let expected: Vec<_> = (0..16)
1799 .map(|_| expected_model.is_limit_filled().unwrap())
1800 .collect();
1801 let first_results: Vec<_> = (0..16).map(|_| first.is_limit_filled().unwrap()).collect();
1802 let second_results: Vec<_> = (0..16).map(|_| second.is_limit_filled().unwrap()).collect();
1803 let has_variation = expected.windows(2).any(|window| window[0] != window[1]);
1804
1805 assert!(has_variation);
1806 assert_eq!(first_results, expected);
1807 assert_eq!(second_results, expected);
1808 }
1809
1810 #[rstest]
1811 fn test_default_fill_model_fill_limit_inside_spread_is_false() {
1812 let model = DefaultFillModel::default();
1813 assert!(!model.fill_limit_inside_spread().unwrap());
1814 }
1815
1816 #[rstest]
1817 fn test_best_price_fill_model_fill_limit_inside_spread_is_true() {
1818 let model = BestPriceFillModel::default();
1819 assert!(model.fill_limit_inside_spread().unwrap());
1820 }
1821
1822 #[rstest]
1823 fn test_one_tick_slippage_fill_model_fill_limit_inside_spread_is_false() {
1824 let model = OneTickSlippageFillModel::default();
1825 assert!(!model.fill_limit_inside_spread().unwrap());
1826 }
1827
1828 #[rstest]
1829 fn test_fill_model_any_fill_limit_inside_spread_dispatch() {
1830 let default = FillModelAny::Default(DefaultFillModel::default());
1831 assert!(!default.fill_limit_inside_spread().unwrap());
1832
1833 let best_price = FillModelAny::BestPrice(BestPriceFillModel::default());
1834 assert!(best_price.fill_limit_inside_spread().unwrap());
1835
1836 let one_tick = FillModelAny::OneTickSlippage(OneTickSlippageFillModel::default());
1837 assert!(!one_tick.fill_limit_inside_spread().unwrap());
1838 }
1839}