1use std::fmt::Display;
17
18#[cfg(all(feature = "simulation", madsim))]
19use madsim::rand::RngCore;
20use nautilus_core::{UnixNanos, correctness::check_in_range_inclusive_f64};
21use nautilus_model::{
22 data::order::BookOrder,
23 enums::{BookType, OrderSide},
24 identifiers::InstrumentId,
25 instruments::{Instrument, InstrumentAny},
26 orderbook::OrderBook,
27 orders::{Order, OrderAny},
28 types::{Price, Quantity, fixed::FIXED_SCALAR, quantity::QuantityRaw},
29};
30use rand::{RngExt, SeedableRng, rngs::StdRng};
31
32const UNLIMITED_LIQUIDITY: f64 = 10_000_000_000.0;
37const UNLIMITED_LIQUIDITY_RAW: QuantityRaw = (UNLIMITED_LIQUIDITY * FIXED_SCALAR) as QuantityRaw;
38
39fn unlimited_liquidity(precision: u8) -> Quantity {
40 Quantity::from_raw(UNLIMITED_LIQUIDITY_RAW, precision)
41}
42
43pub trait FillModel {
44 fn is_limit_filled(&mut self) -> bool;
46
47 fn is_slipped(&mut self) -> bool;
49
50 fn fill_limit_inside_spread(&self) -> bool {
56 false
57 }
58
59 fn get_orderbook_for_fill_simulation(
67 &mut self,
68 instrument: &InstrumentAny,
69 order: &OrderAny,
70 best_bid: Price,
71 best_ask: Price,
72 ) -> Option<OrderBook>;
73}
74
75#[derive(Debug)]
76pub struct ProbabilisticFillState {
77 prob_fill_on_limit: f64,
78 prob_slippage: f64,
79 random_seed: Option<u64>,
80 rng: StdRng,
81}
82
83impl ProbabilisticFillState {
84 pub fn new(
90 prob_fill_on_limit: f64,
91 prob_slippage: f64,
92 random_seed: Option<u64>,
93 ) -> anyhow::Result<Self> {
94 check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")?;
95 check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage")?;
96 let rng = match random_seed {
97 Some(seed) => StdRng::seed_from_u64(seed),
98 None => default_std_rng(),
99 };
100 Ok(Self {
101 prob_fill_on_limit,
102 prob_slippage,
103 random_seed,
104 rng,
105 })
106 }
107
108 pub fn is_limit_filled(&mut self) -> bool {
109 self.event_success(self.prob_fill_on_limit)
110 }
111
112 pub fn is_slipped(&mut self) -> bool {
113 self.event_success(self.prob_slippage)
114 }
115
116 pub fn random_bool(&mut self, probability: f64) -> bool {
117 self.event_success(probability)
118 }
119
120 fn event_success(&mut self, probability: f64) -> bool {
121 match probability {
122 0.0 => false,
123 1.0 => true,
124 _ => self.rng.random_bool(probability),
125 }
126 }
127}
128
129impl Clone for ProbabilisticFillState {
130 fn clone(&self) -> Self {
131 Self::new(
132 self.prob_fill_on_limit,
133 self.prob_slippage,
134 self.random_seed,
135 )
136 .expect("ProbabilisticFillState clone should not fail with valid parameters")
137 }
138}
139
140fn default_std_rng() -> StdRng {
141 #[cfg(all(feature = "simulation", madsim))]
142 {
143 if madsim::runtime::Handle::try_current().is_ok() {
148 let mut seed = [0u8; 32];
149 madsim::rand::thread_rng().fill_bytes(&mut seed);
150 return StdRng::from_seed(seed);
151 }
152 }
153
154 StdRng::from_rng(&mut rand::rng()) }
156
157fn build_l2_book(instrument_id: InstrumentId) -> OrderBook {
158 OrderBook::new(instrument_id, BookType::L2_MBP)
159}
160
161fn add_order(book: &mut OrderBook, side: OrderSide, price: Price, size: Quantity, order_id: u64) {
162 let order = BookOrder::new(side, price, size, order_id);
163 book.add(order, 0, 0, UnixNanos::default());
164}
165
166#[derive(Debug)]
167#[cfg_attr(
168 feature = "python",
169 pyo3::pyclass(
170 module = "nautilus_trader.core.nautilus_pyo3.execution",
171 unsendable,
172 from_py_object
173 )
174)]
175#[cfg_attr(
176 feature = "python",
177 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
178)]
179pub struct DefaultFillModel {
180 state: ProbabilisticFillState,
181}
182
183impl DefaultFillModel {
184 pub fn new(
190 prob_fill_on_limit: f64,
191 prob_slippage: f64,
192 random_seed: Option<u64>,
193 ) -> anyhow::Result<Self> {
194 Ok(Self {
195 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
196 })
197 }
198}
199
200impl Clone for DefaultFillModel {
201 fn clone(&self) -> Self {
202 Self {
203 state: self.state.clone(),
204 }
205 }
206}
207
208impl Default for DefaultFillModel {
209 fn default() -> Self {
210 Self::new(1.0, 0.0, None).unwrap()
211 }
212}
213
214impl Display for DefaultFillModel {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(
217 f,
218 "DefaultFillModel(prob_fill_on_limit: {}, prob_slippage: {})",
219 self.state.prob_fill_on_limit, self.state.prob_slippage
220 )
221 }
222}
223
224impl FillModel for DefaultFillModel {
225 fn is_limit_filled(&mut self) -> bool {
226 self.state.is_limit_filled()
227 }
228
229 fn is_slipped(&mut self) -> bool {
230 self.state.is_slipped()
231 }
232
233 fn get_orderbook_for_fill_simulation(
234 &mut self,
235 _instrument: &InstrumentAny,
236 _order: &OrderAny,
237 _best_bid: Price,
238 _best_ask: Price,
239 ) -> Option<OrderBook> {
240 None
241 }
242}
243
244#[derive(Debug)]
246#[cfg_attr(
247 feature = "python",
248 pyo3::pyclass(
249 module = "nautilus_trader.core.nautilus_pyo3.execution",
250 unsendable,
251 from_py_object
252 )
253)]
254#[cfg_attr(
255 feature = "python",
256 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
257)]
258pub struct BestPriceFillModel {
259 state: ProbabilisticFillState,
260}
261
262impl BestPriceFillModel {
263 pub fn new(
269 prob_fill_on_limit: f64,
270 prob_slippage: f64,
271 random_seed: Option<u64>,
272 ) -> anyhow::Result<Self> {
273 Ok(Self {
274 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
275 })
276 }
277}
278
279impl Clone for BestPriceFillModel {
280 fn clone(&self) -> Self {
281 Self {
282 state: self.state.clone(),
283 }
284 }
285}
286
287impl Default for BestPriceFillModel {
288 fn default() -> Self {
289 Self::new(1.0, 0.0, None).unwrap()
290 }
291}
292
293impl FillModel for BestPriceFillModel {
294 fn is_limit_filled(&mut self) -> bool {
295 self.state.is_limit_filled()
296 }
297
298 fn is_slipped(&mut self) -> bool {
299 self.state.is_slipped()
300 }
301
302 fn fill_limit_inside_spread(&self) -> bool {
303 true
304 }
305
306 fn get_orderbook_for_fill_simulation(
307 &mut self,
308 instrument: &InstrumentAny,
309 _order: &OrderAny,
310 best_bid: Price,
311 best_ask: Price,
312 ) -> Option<OrderBook> {
313 let mut book = build_l2_book(instrument.id());
314 let size_prec = instrument.size_precision();
315 add_order(
316 &mut book,
317 OrderSide::Buy,
318 best_bid,
319 unlimited_liquidity(size_prec),
320 1,
321 );
322 add_order(
323 &mut book,
324 OrderSide::Sell,
325 best_ask,
326 unlimited_liquidity(size_prec),
327 2,
328 );
329 Some(book)
330 }
331}
332
333#[derive(Debug)]
335#[cfg_attr(
336 feature = "python",
337 pyo3::pyclass(
338 module = "nautilus_trader.core.nautilus_pyo3.execution",
339 unsendable,
340 from_py_object
341 )
342)]
343#[cfg_attr(
344 feature = "python",
345 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
346)]
347pub struct OneTickSlippageFillModel {
348 state: ProbabilisticFillState,
349}
350
351impl OneTickSlippageFillModel {
352 pub fn new(
358 prob_fill_on_limit: f64,
359 prob_slippage: f64,
360 random_seed: Option<u64>,
361 ) -> anyhow::Result<Self> {
362 Ok(Self {
363 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
364 })
365 }
366}
367
368impl Clone for OneTickSlippageFillModel {
369 fn clone(&self) -> Self {
370 Self {
371 state: self.state.clone(),
372 }
373 }
374}
375
376impl Default for OneTickSlippageFillModel {
377 fn default() -> Self {
378 Self::new(1.0, 0.0, None).unwrap()
379 }
380}
381
382impl FillModel for OneTickSlippageFillModel {
383 fn is_limit_filled(&mut self) -> bool {
384 self.state.is_limit_filled()
385 }
386
387 fn is_slipped(&mut self) -> bool {
388 self.state.is_slipped()
389 }
390
391 fn get_orderbook_for_fill_simulation(
392 &mut self,
393 instrument: &InstrumentAny,
394 _order: &OrderAny,
395 best_bid: Price,
396 best_ask: Price,
397 ) -> Option<OrderBook> {
398 let tick = instrument.price_increment();
399 let size_prec = instrument.size_precision();
400 let mut book = build_l2_book(instrument.id());
401
402 add_order(
403 &mut book,
404 OrderSide::Buy,
405 best_bid - tick,
406 unlimited_liquidity(size_prec),
407 1,
408 );
409 add_order(
410 &mut book,
411 OrderSide::Sell,
412 best_ask + tick,
413 unlimited_liquidity(size_prec),
414 2,
415 );
416 Some(book)
417 }
418}
419
420#[derive(Debug)]
422#[cfg_attr(
423 feature = "python",
424 pyo3::pyclass(
425 module = "nautilus_trader.core.nautilus_pyo3.execution",
426 unsendable,
427 from_py_object
428 )
429)]
430#[cfg_attr(
431 feature = "python",
432 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
433)]
434pub struct ProbabilisticFillModel {
435 state: ProbabilisticFillState,
436}
437
438impl ProbabilisticFillModel {
439 pub fn new(
445 prob_fill_on_limit: f64,
446 prob_slippage: f64,
447 random_seed: Option<u64>,
448 ) -> anyhow::Result<Self> {
449 Ok(Self {
450 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
451 })
452 }
453}
454
455impl Clone for ProbabilisticFillModel {
456 fn clone(&self) -> Self {
457 Self {
458 state: self.state.clone(),
459 }
460 }
461}
462
463impl Default for ProbabilisticFillModel {
464 fn default() -> Self {
465 Self::new(1.0, 0.0, None).unwrap()
466 }
467}
468
469impl FillModel for ProbabilisticFillModel {
470 fn is_limit_filled(&mut self) -> bool {
471 self.state.is_limit_filled()
472 }
473
474 fn is_slipped(&mut self) -> bool {
475 self.state.is_slipped()
476 }
477
478 fn get_orderbook_for_fill_simulation(
479 &mut self,
480 instrument: &InstrumentAny,
481 _order: &OrderAny,
482 best_bid: Price,
483 best_ask: Price,
484 ) -> Option<OrderBook> {
485 let tick = instrument.price_increment();
486 let size_prec = instrument.size_precision();
487 let mut book = build_l2_book(instrument.id());
488
489 if self.state.random_bool(0.5) {
490 add_order(
491 &mut book,
492 OrderSide::Buy,
493 best_bid,
494 unlimited_liquidity(size_prec),
495 1,
496 );
497 add_order(
498 &mut book,
499 OrderSide::Sell,
500 best_ask,
501 unlimited_liquidity(size_prec),
502 2,
503 );
504 } else {
505 add_order(
506 &mut book,
507 OrderSide::Buy,
508 best_bid - tick,
509 unlimited_liquidity(size_prec),
510 1,
511 );
512 add_order(
513 &mut book,
514 OrderSide::Sell,
515 best_ask + tick,
516 unlimited_liquidity(size_prec),
517 2,
518 );
519 }
520 Some(book)
521 }
522}
523
524#[derive(Debug)]
526#[cfg_attr(
527 feature = "python",
528 pyo3::pyclass(
529 module = "nautilus_trader.core.nautilus_pyo3.execution",
530 unsendable,
531 from_py_object
532 )
533)]
534#[cfg_attr(
535 feature = "python",
536 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
537)]
538pub struct TwoTierFillModel {
539 state: ProbabilisticFillState,
540}
541
542impl TwoTierFillModel {
543 pub fn new(
549 prob_fill_on_limit: f64,
550 prob_slippage: f64,
551 random_seed: Option<u64>,
552 ) -> anyhow::Result<Self> {
553 Ok(Self {
554 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
555 })
556 }
557}
558
559impl Clone for TwoTierFillModel {
560 fn clone(&self) -> Self {
561 Self {
562 state: self.state.clone(),
563 }
564 }
565}
566
567impl Default for TwoTierFillModel {
568 fn default() -> Self {
569 Self::new(1.0, 0.0, None).unwrap()
570 }
571}
572
573impl FillModel for TwoTierFillModel {
574 fn is_limit_filled(&mut self) -> bool {
575 self.state.is_limit_filled()
576 }
577
578 fn is_slipped(&mut self) -> bool {
579 self.state.is_slipped()
580 }
581
582 fn get_orderbook_for_fill_simulation(
583 &mut self,
584 instrument: &InstrumentAny,
585 _order: &OrderAny,
586 best_bid: Price,
587 best_ask: Price,
588 ) -> Option<OrderBook> {
589 let tick = instrument.price_increment();
590 let size_prec = instrument.size_precision();
591 let mut book = build_l2_book(instrument.id());
592
593 add_order(
594 &mut book,
595 OrderSide::Buy,
596 best_bid,
597 Quantity::new(10.0, size_prec),
598 1,
599 );
600 add_order(
601 &mut book,
602 OrderSide::Sell,
603 best_ask,
604 Quantity::new(10.0, size_prec),
605 2,
606 );
607 add_order(
608 &mut book,
609 OrderSide::Buy,
610 best_bid - tick,
611 unlimited_liquidity(size_prec),
612 3,
613 );
614 add_order(
615 &mut book,
616 OrderSide::Sell,
617 best_ask + tick,
618 unlimited_liquidity(size_prec),
619 4,
620 );
621 Some(book)
622 }
623}
624
625#[derive(Debug)]
627#[cfg_attr(
628 feature = "python",
629 pyo3::pyclass(
630 module = "nautilus_trader.core.nautilus_pyo3.execution",
631 unsendable,
632 from_py_object
633 )
634)]
635#[cfg_attr(
636 feature = "python",
637 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
638)]
639pub struct ThreeTierFillModel {
640 state: ProbabilisticFillState,
641}
642
643impl ThreeTierFillModel {
644 pub fn new(
650 prob_fill_on_limit: f64,
651 prob_slippage: f64,
652 random_seed: Option<u64>,
653 ) -> anyhow::Result<Self> {
654 Ok(Self {
655 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
656 })
657 }
658}
659
660impl Clone for ThreeTierFillModel {
661 fn clone(&self) -> Self {
662 Self {
663 state: self.state.clone(),
664 }
665 }
666}
667
668impl Default for ThreeTierFillModel {
669 fn default() -> Self {
670 Self::new(1.0, 0.0, None).unwrap()
671 }
672}
673
674impl FillModel for ThreeTierFillModel {
675 fn is_limit_filled(&mut self) -> bool {
676 self.state.is_limit_filled()
677 }
678
679 fn is_slipped(&mut self) -> bool {
680 self.state.is_slipped()
681 }
682
683 fn get_orderbook_for_fill_simulation(
684 &mut self,
685 instrument: &InstrumentAny,
686 _order: &OrderAny,
687 best_bid: Price,
688 best_ask: Price,
689 ) -> Option<OrderBook> {
690 let tick = instrument.price_increment();
691 let two_ticks = tick + tick;
692 let size_prec = instrument.size_precision();
693 let mut book = build_l2_book(instrument.id());
694
695 add_order(
696 &mut book,
697 OrderSide::Buy,
698 best_bid,
699 Quantity::new(50.0, size_prec),
700 1,
701 );
702 add_order(
703 &mut book,
704 OrderSide::Sell,
705 best_ask,
706 Quantity::new(50.0, size_prec),
707 2,
708 );
709 add_order(
710 &mut book,
711 OrderSide::Buy,
712 best_bid - tick,
713 Quantity::new(30.0, size_prec),
714 3,
715 );
716 add_order(
717 &mut book,
718 OrderSide::Sell,
719 best_ask + tick,
720 Quantity::new(30.0, size_prec),
721 4,
722 );
723 add_order(
724 &mut book,
725 OrderSide::Buy,
726 best_bid - two_ticks,
727 Quantity::new(20.0, size_prec),
728 5,
729 );
730 add_order(
731 &mut book,
732 OrderSide::Sell,
733 best_ask + two_ticks,
734 Quantity::new(20.0, size_prec),
735 6,
736 );
737 Some(book)
738 }
739}
740
741#[derive(Debug)]
743#[cfg_attr(
744 feature = "python",
745 pyo3::pyclass(
746 module = "nautilus_trader.core.nautilus_pyo3.execution",
747 unsendable,
748 from_py_object
749 )
750)]
751#[cfg_attr(
752 feature = "python",
753 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
754)]
755pub struct LimitOrderPartialFillModel {
756 state: ProbabilisticFillState,
757}
758
759impl LimitOrderPartialFillModel {
760 pub fn new(
766 prob_fill_on_limit: f64,
767 prob_slippage: f64,
768 random_seed: Option<u64>,
769 ) -> anyhow::Result<Self> {
770 Ok(Self {
771 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
772 })
773 }
774}
775
776impl Clone for LimitOrderPartialFillModel {
777 fn clone(&self) -> Self {
778 Self {
779 state: self.state.clone(),
780 }
781 }
782}
783
784impl Default for LimitOrderPartialFillModel {
785 fn default() -> Self {
786 Self::new(1.0, 0.0, None).unwrap()
787 }
788}
789
790impl FillModel for LimitOrderPartialFillModel {
791 fn is_limit_filled(&mut self) -> bool {
792 self.state.is_limit_filled()
793 }
794
795 fn is_slipped(&mut self) -> bool {
796 self.state.is_slipped()
797 }
798
799 fn get_orderbook_for_fill_simulation(
800 &mut self,
801 instrument: &InstrumentAny,
802 _order: &OrderAny,
803 best_bid: Price,
804 best_ask: Price,
805 ) -> Option<OrderBook> {
806 let tick = instrument.price_increment();
807 let size_prec = instrument.size_precision();
808 let mut book = build_l2_book(instrument.id());
809
810 add_order(
811 &mut book,
812 OrderSide::Buy,
813 best_bid,
814 Quantity::new(5.0, size_prec),
815 1,
816 );
817 add_order(
818 &mut book,
819 OrderSide::Sell,
820 best_ask,
821 Quantity::new(5.0, size_prec),
822 2,
823 );
824 add_order(
825 &mut book,
826 OrderSide::Buy,
827 best_bid - tick,
828 unlimited_liquidity(size_prec),
829 3,
830 );
831 add_order(
832 &mut book,
833 OrderSide::Sell,
834 best_ask + tick,
835 unlimited_liquidity(size_prec),
836 4,
837 );
838 Some(book)
839 }
840}
841
842#[derive(Debug)]
845#[cfg_attr(
846 feature = "python",
847 pyo3::pyclass(
848 module = "nautilus_trader.core.nautilus_pyo3.execution",
849 unsendable,
850 from_py_object
851 )
852)]
853#[cfg_attr(
854 feature = "python",
855 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
856)]
857pub struct SizeAwareFillModel {
858 state: ProbabilisticFillState,
859}
860
861impl SizeAwareFillModel {
862 pub fn new(
868 prob_fill_on_limit: f64,
869 prob_slippage: f64,
870 random_seed: Option<u64>,
871 ) -> anyhow::Result<Self> {
872 Ok(Self {
873 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
874 })
875 }
876}
877
878impl Clone for SizeAwareFillModel {
879 fn clone(&self) -> Self {
880 Self {
881 state: self.state.clone(),
882 }
883 }
884}
885
886impl Default for SizeAwareFillModel {
887 fn default() -> Self {
888 Self::new(1.0, 0.0, None).unwrap()
889 }
890}
891
892impl FillModel for SizeAwareFillModel {
893 fn is_limit_filled(&mut self) -> bool {
894 self.state.is_limit_filled()
895 }
896
897 fn is_slipped(&mut self) -> bool {
898 self.state.is_slipped()
899 }
900
901 fn get_orderbook_for_fill_simulation(
902 &mut self,
903 instrument: &InstrumentAny,
904 order: &OrderAny,
905 best_bid: Price,
906 best_ask: Price,
907 ) -> Option<OrderBook> {
908 let tick = instrument.price_increment();
909 let size_prec = instrument.size_precision();
910 let mut book = build_l2_book(instrument.id());
911
912 let threshold = Quantity::new(10.0, size_prec);
913 if order.quantity() <= threshold {
914 add_order(
916 &mut book,
917 OrderSide::Buy,
918 best_bid,
919 Quantity::new(50.0, size_prec),
920 1,
921 );
922 add_order(
923 &mut book,
924 OrderSide::Sell,
925 best_ask,
926 Quantity::new(50.0, size_prec),
927 2,
928 );
929 } else {
930 let remaining = order.quantity() - threshold;
932 add_order(&mut book, OrderSide::Buy, best_bid, threshold, 1);
933 add_order(&mut book, OrderSide::Sell, best_ask, threshold, 2);
934 add_order(&mut book, OrderSide::Buy, best_bid - tick, remaining, 3);
935 add_order(&mut book, OrderSide::Sell, best_ask + tick, remaining, 4);
936 }
937 Some(book)
938 }
939}
940
941#[derive(Debug)]
943#[cfg_attr(
944 feature = "python",
945 pyo3::pyclass(
946 module = "nautilus_trader.core.nautilus_pyo3.execution",
947 unsendable,
948 from_py_object
949 )
950)]
951#[cfg_attr(
952 feature = "python",
953 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
954)]
955pub struct CompetitionAwareFillModel {
956 state: ProbabilisticFillState,
957 liquidity_factor: f64,
958}
959
960impl CompetitionAwareFillModel {
961 pub fn new(
967 prob_fill_on_limit: f64,
968 prob_slippage: f64,
969 random_seed: Option<u64>,
970 liquidity_factor: f64,
971 ) -> anyhow::Result<Self> {
972 Ok(Self {
973 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
974 liquidity_factor,
975 })
976 }
977}
978
979impl Clone for CompetitionAwareFillModel {
980 fn clone(&self) -> Self {
981 Self {
982 state: self.state.clone(),
983 liquidity_factor: self.liquidity_factor,
984 }
985 }
986}
987
988impl Default for CompetitionAwareFillModel {
989 fn default() -> Self {
990 Self::new(1.0, 0.0, None, 0.3).unwrap()
991 }
992}
993
994impl FillModel for CompetitionAwareFillModel {
995 fn is_limit_filled(&mut self) -> bool {
996 self.state.is_limit_filled()
997 }
998
999 fn is_slipped(&mut self) -> bool {
1000 self.state.is_slipped()
1001 }
1002
1003 fn get_orderbook_for_fill_simulation(
1004 &mut self,
1005 instrument: &InstrumentAny,
1006 _order: &OrderAny,
1007 best_bid: Price,
1008 best_ask: Price,
1009 ) -> Option<OrderBook> {
1010 let size_prec = instrument.size_precision();
1011 let mut book = build_l2_book(instrument.id());
1012
1013 let typical_volume = 1000.0;
1014
1015 let available_bid = (typical_volume * self.liquidity_factor).max(1.0);
1017 let available_ask = (typical_volume * self.liquidity_factor).max(1.0);
1018
1019 add_order(
1020 &mut book,
1021 OrderSide::Buy,
1022 best_bid,
1023 Quantity::new(available_bid, size_prec),
1024 1,
1025 );
1026 add_order(
1027 &mut book,
1028 OrderSide::Sell,
1029 best_ask,
1030 Quantity::new(available_ask, size_prec),
1031 2,
1032 );
1033 Some(book)
1034 }
1035}
1036
1037#[derive(Debug)]
1040#[cfg_attr(
1041 feature = "python",
1042 pyo3::pyclass(
1043 module = "nautilus_trader.core.nautilus_pyo3.execution",
1044 unsendable,
1045 from_py_object
1046 )
1047)]
1048#[cfg_attr(
1049 feature = "python",
1050 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1051)]
1052pub struct VolumeSensitiveFillModel {
1053 state: ProbabilisticFillState,
1054 recent_volume: f64,
1055}
1056
1057impl VolumeSensitiveFillModel {
1058 pub fn new(
1064 prob_fill_on_limit: f64,
1065 prob_slippage: f64,
1066 random_seed: Option<u64>,
1067 ) -> anyhow::Result<Self> {
1068 Ok(Self {
1069 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1070 recent_volume: 1000.0,
1071 })
1072 }
1073
1074 pub fn set_recent_volume(&mut self, volume: f64) {
1075 self.recent_volume = volume;
1076 }
1077}
1078
1079impl Clone for VolumeSensitiveFillModel {
1080 fn clone(&self) -> Self {
1081 Self {
1082 state: self.state.clone(),
1083 recent_volume: self.recent_volume,
1084 }
1085 }
1086}
1087
1088impl Default for VolumeSensitiveFillModel {
1089 fn default() -> Self {
1090 Self::new(1.0, 0.0, None).unwrap()
1091 }
1092}
1093
1094impl FillModel for VolumeSensitiveFillModel {
1095 fn is_limit_filled(&mut self) -> bool {
1096 self.state.is_limit_filled()
1097 }
1098
1099 fn is_slipped(&mut self) -> bool {
1100 self.state.is_slipped()
1101 }
1102
1103 fn get_orderbook_for_fill_simulation(
1104 &mut self,
1105 instrument: &InstrumentAny,
1106 _order: &OrderAny,
1107 best_bid: Price,
1108 best_ask: Price,
1109 ) -> Option<OrderBook> {
1110 let tick = instrument.price_increment();
1111 let size_prec = instrument.size_precision();
1112 let mut book = build_l2_book(instrument.id());
1113
1114 let available_volume = (self.recent_volume * 0.25).max(1.0);
1116
1117 add_order(
1118 &mut book,
1119 OrderSide::Buy,
1120 best_bid,
1121 Quantity::new(available_volume, size_prec),
1122 1,
1123 );
1124 add_order(
1125 &mut book,
1126 OrderSide::Sell,
1127 best_ask,
1128 Quantity::new(available_volume, size_prec),
1129 2,
1130 );
1131 add_order(
1132 &mut book,
1133 OrderSide::Buy,
1134 best_bid - tick,
1135 unlimited_liquidity(size_prec),
1136 3,
1137 );
1138 add_order(
1139 &mut book,
1140 OrderSide::Sell,
1141 best_ask + tick,
1142 unlimited_liquidity(size_prec),
1143 4,
1144 );
1145 Some(book)
1146 }
1147}
1148
1149#[derive(Debug)]
1152#[cfg_attr(
1153 feature = "python",
1154 pyo3::pyclass(
1155 module = "nautilus_trader.core.nautilus_pyo3.execution",
1156 unsendable,
1157 from_py_object
1158 )
1159)]
1160#[cfg_attr(
1161 feature = "python",
1162 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1163)]
1164pub struct MarketHoursFillModel {
1165 state: ProbabilisticFillState,
1166 is_low_liquidity: bool,
1167}
1168
1169impl MarketHoursFillModel {
1170 pub fn new(
1176 prob_fill_on_limit: f64,
1177 prob_slippage: f64,
1178 random_seed: Option<u64>,
1179 ) -> anyhow::Result<Self> {
1180 Ok(Self {
1181 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1182 is_low_liquidity: false,
1183 })
1184 }
1185
1186 pub fn set_low_liquidity_period(&mut self, is_low_liquidity: bool) {
1187 self.is_low_liquidity = is_low_liquidity;
1188 }
1189
1190 pub fn is_low_liquidity_period(&self) -> bool {
1191 self.is_low_liquidity
1192 }
1193}
1194
1195impl Clone for MarketHoursFillModel {
1196 fn clone(&self) -> Self {
1197 Self {
1198 state: self.state.clone(),
1199 is_low_liquidity: self.is_low_liquidity,
1200 }
1201 }
1202}
1203
1204impl Default for MarketHoursFillModel {
1205 fn default() -> Self {
1206 Self::new(1.0, 0.0, None).unwrap()
1207 }
1208}
1209
1210impl FillModel for MarketHoursFillModel {
1211 fn is_limit_filled(&mut self) -> bool {
1212 self.state.is_limit_filled()
1213 }
1214
1215 fn is_slipped(&mut self) -> bool {
1216 self.state.is_slipped()
1217 }
1218
1219 fn get_orderbook_for_fill_simulation(
1220 &mut self,
1221 instrument: &InstrumentAny,
1222 _order: &OrderAny,
1223 best_bid: Price,
1224 best_ask: Price,
1225 ) -> Option<OrderBook> {
1226 let tick = instrument.price_increment();
1227 let size_prec = instrument.size_precision();
1228 let mut book = build_l2_book(instrument.id());
1229 let normal_volume = 500.0;
1230
1231 if self.is_low_liquidity {
1232 add_order(
1233 &mut book,
1234 OrderSide::Buy,
1235 best_bid - tick,
1236 Quantity::new(normal_volume, size_prec),
1237 1,
1238 );
1239 add_order(
1240 &mut book,
1241 OrderSide::Sell,
1242 best_ask + tick,
1243 Quantity::new(normal_volume, size_prec),
1244 2,
1245 );
1246 } else {
1247 add_order(
1248 &mut book,
1249 OrderSide::Buy,
1250 best_bid,
1251 Quantity::new(normal_volume, size_prec),
1252 1,
1253 );
1254 add_order(
1255 &mut book,
1256 OrderSide::Sell,
1257 best_ask,
1258 Quantity::new(normal_volume, size_prec),
1259 2,
1260 );
1261 }
1262 Some(book)
1263 }
1264}
1265
1266#[derive(Clone, Debug)]
1267pub enum FillModelAny {
1268 Default(DefaultFillModel),
1269 BestPrice(BestPriceFillModel),
1270 OneTickSlippage(OneTickSlippageFillModel),
1271 Probabilistic(ProbabilisticFillModel),
1272 TwoTier(TwoTierFillModel),
1273 ThreeTier(ThreeTierFillModel),
1274 LimitOrderPartialFill(LimitOrderPartialFillModel),
1275 SizeAware(SizeAwareFillModel),
1276 CompetitionAware(CompetitionAwareFillModel),
1277 VolumeSensitive(VolumeSensitiveFillModel),
1278 MarketHours(MarketHoursFillModel),
1279}
1280
1281impl FillModel for FillModelAny {
1282 fn is_limit_filled(&mut self) -> bool {
1283 match self {
1284 Self::Default(m) => m.is_limit_filled(),
1285 Self::BestPrice(m) => m.is_limit_filled(),
1286 Self::OneTickSlippage(m) => m.is_limit_filled(),
1287 Self::Probabilistic(m) => m.is_limit_filled(),
1288 Self::TwoTier(m) => m.is_limit_filled(),
1289 Self::ThreeTier(m) => m.is_limit_filled(),
1290 Self::LimitOrderPartialFill(m) => m.is_limit_filled(),
1291 Self::SizeAware(m) => m.is_limit_filled(),
1292 Self::CompetitionAware(m) => m.is_limit_filled(),
1293 Self::VolumeSensitive(m) => m.is_limit_filled(),
1294 Self::MarketHours(m) => m.is_limit_filled(),
1295 }
1296 }
1297
1298 fn fill_limit_inside_spread(&self) -> bool {
1299 match self {
1300 Self::Default(m) => m.fill_limit_inside_spread(),
1301 Self::BestPrice(m) => m.fill_limit_inside_spread(),
1302 Self::OneTickSlippage(m) => m.fill_limit_inside_spread(),
1303 Self::Probabilistic(m) => m.fill_limit_inside_spread(),
1304 Self::TwoTier(m) => m.fill_limit_inside_spread(),
1305 Self::ThreeTier(m) => m.fill_limit_inside_spread(),
1306 Self::LimitOrderPartialFill(m) => m.fill_limit_inside_spread(),
1307 Self::SizeAware(m) => m.fill_limit_inside_spread(),
1308 Self::CompetitionAware(m) => m.fill_limit_inside_spread(),
1309 Self::VolumeSensitive(m) => m.fill_limit_inside_spread(),
1310 Self::MarketHours(m) => m.fill_limit_inside_spread(),
1311 }
1312 }
1313
1314 fn is_slipped(&mut self) -> bool {
1315 match self {
1316 Self::Default(m) => m.is_slipped(),
1317 Self::BestPrice(m) => m.is_slipped(),
1318 Self::OneTickSlippage(m) => m.is_slipped(),
1319 Self::Probabilistic(m) => m.is_slipped(),
1320 Self::TwoTier(m) => m.is_slipped(),
1321 Self::ThreeTier(m) => m.is_slipped(),
1322 Self::LimitOrderPartialFill(m) => m.is_slipped(),
1323 Self::SizeAware(m) => m.is_slipped(),
1324 Self::CompetitionAware(m) => m.is_slipped(),
1325 Self::VolumeSensitive(m) => m.is_slipped(),
1326 Self::MarketHours(m) => m.is_slipped(),
1327 }
1328 }
1329
1330 fn get_orderbook_for_fill_simulation(
1331 &mut self,
1332 instrument: &InstrumentAny,
1333 order: &OrderAny,
1334 best_bid: Price,
1335 best_ask: Price,
1336 ) -> Option<OrderBook> {
1337 match self {
1338 Self::Default(m) => {
1339 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1340 }
1341 Self::BestPrice(m) => {
1342 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1343 }
1344 Self::OneTickSlippage(m) => {
1345 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1346 }
1347 Self::Probabilistic(m) => {
1348 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1349 }
1350 Self::TwoTier(m) => {
1351 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1352 }
1353 Self::ThreeTier(m) => {
1354 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1355 }
1356 Self::LimitOrderPartialFill(m) => {
1357 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1358 }
1359 Self::SizeAware(m) => {
1360 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1361 }
1362 Self::CompetitionAware(m) => {
1363 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1364 }
1365 Self::VolumeSensitive(m) => {
1366 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1367 }
1368 Self::MarketHours(m) => {
1369 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1370 }
1371 }
1372 }
1373}
1374
1375impl Default for FillModelAny {
1376 fn default() -> Self {
1377 Self::Default(DefaultFillModel::default())
1378 }
1379}
1380
1381impl Display for FillModelAny {
1382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1383 match self {
1384 Self::Default(m) => write!(f, "{m}"),
1385 Self::BestPrice(_) => write!(f, "BestPriceFillModel"),
1386 Self::OneTickSlippage(_) => write!(f, "OneTickSlippageFillModel"),
1387 Self::Probabilistic(_) => write!(f, "ProbabilisticFillModel"),
1388 Self::TwoTier(_) => write!(f, "TwoTierFillModel"),
1389 Self::ThreeTier(_) => write!(f, "ThreeTierFillModel"),
1390 Self::LimitOrderPartialFill(_) => write!(f, "LimitOrderPartialFillModel"),
1391 Self::SizeAware(_) => write!(f, "SizeAwareFillModel"),
1392 Self::CompetitionAware(_) => write!(f, "CompetitionAwareFillModel"),
1393 Self::VolumeSensitive(_) => write!(f, "VolumeSensitiveFillModel"),
1394 Self::MarketHours(_) => write!(f, "MarketHoursFillModel"),
1395 }
1396 }
1397}
1398
1399#[cfg(test)]
1400mod tests {
1401 use nautilus_core::correctness::CorrectnessError;
1402 use nautilus_model::{
1403 enums::OrderType, instruments::stubs::audusd_sim, orders::builder::OrderTestBuilder,
1404 };
1405 use rstest::{fixture, rstest};
1406
1407 use super::*;
1408
1409 #[fixture]
1410 fn fill_model() -> DefaultFillModel {
1411 let seed = 42;
1412 DefaultFillModel::new(0.5, 0.1, Some(seed)).unwrap()
1413 }
1414
1415 #[rstest]
1416 fn test_fill_model_param_prob_fill_on_limit_error() {
1417 let error = DefaultFillModel::new(1.1, 0.1, None).unwrap_err();
1418
1419 assert_eq!(
1420 error.downcast_ref::<CorrectnessError>(),
1421 Some(&CorrectnessError::OutOfRange {
1422 param: "prob_fill_on_limit".to_string(),
1423 min: "0".to_string(),
1424 max: "1".to_string(),
1425 value: "1.1".to_string(),
1426 type_name: "f64",
1427 })
1428 );
1429 assert_eq!(
1430 error.to_string(),
1431 "invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
1432 );
1433 }
1434
1435 #[rstest]
1436 fn test_fill_model_param_prob_slippage_error() {
1437 let error = DefaultFillModel::new(0.5, 1.1, None).unwrap_err();
1438
1439 assert_eq!(
1440 error.downcast_ref::<CorrectnessError>(),
1441 Some(&CorrectnessError::OutOfRange {
1442 param: "prob_slippage".to_string(),
1443 min: "0".to_string(),
1444 max: "1".to_string(),
1445 value: "1.1".to_string(),
1446 type_name: "f64",
1447 })
1448 );
1449 assert_eq!(
1450 error.to_string(),
1451 "invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
1452 );
1453 }
1454
1455 #[rstest]
1456 fn test_fill_model_is_limit_filled(mut fill_model: DefaultFillModel) {
1457 let result = fill_model.is_limit_filled();
1459 assert!(!result);
1460 }
1461
1462 #[rstest]
1463 fn test_fill_model_is_slipped(mut fill_model: DefaultFillModel) {
1464 let result = fill_model.is_slipped();
1466 assert!(!result);
1467 }
1468
1469 #[rstest]
1470 fn test_default_fill_model_returns_none() {
1471 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1472 let order = OrderTestBuilder::new(OrderType::Market)
1473 .instrument_id(instrument.id())
1474 .side(OrderSide::Buy)
1475 .quantity(Quantity::from(100_000))
1476 .build();
1477
1478 let mut model = DefaultFillModel::default();
1479 let result = model.get_orderbook_for_fill_simulation(
1480 &instrument,
1481 &order,
1482 Price::from("0.80000"),
1483 Price::from("0.80010"),
1484 );
1485 assert!(result.is_none());
1486 }
1487
1488 #[rstest]
1489 fn test_best_price_fill_model_returns_book() {
1490 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1491 let order = OrderTestBuilder::new(OrderType::Market)
1492 .instrument_id(instrument.id())
1493 .side(OrderSide::Buy)
1494 .quantity(Quantity::from(100_000))
1495 .build();
1496
1497 let mut model = BestPriceFillModel::default();
1498 let result = model.get_orderbook_for_fill_simulation(
1499 &instrument,
1500 &order,
1501 Price::from("0.80000"),
1502 Price::from("0.80010"),
1503 );
1504 assert!(result.is_some());
1505 let book = result.unwrap();
1506 assert_eq!(book.best_bid_price().unwrap(), Price::from("0.80000"));
1507 assert_eq!(book.best_ask_price().unwrap(), Price::from("0.80010"));
1508 }
1509
1510 #[rstest]
1511 fn test_one_tick_slippage_fill_model() {
1512 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1513 let order = OrderTestBuilder::new(OrderType::Market)
1514 .instrument_id(instrument.id())
1515 .side(OrderSide::Buy)
1516 .quantity(Quantity::from(100_000))
1517 .build();
1518
1519 let tick = instrument.price_increment();
1520 let best_bid = Price::from("0.80000");
1521 let best_ask = Price::from("0.80010");
1522
1523 let mut model = OneTickSlippageFillModel::default();
1524 let result =
1525 model.get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask);
1526 assert!(result.is_some());
1527 let book = result.unwrap();
1528
1529 assert_eq!(book.best_bid_price().unwrap(), best_bid - tick);
1530 assert_eq!(book.best_ask_price().unwrap(), best_ask + tick);
1531 }
1532
1533 #[rstest]
1534 fn test_fill_model_any_dispatch() {
1535 let model = FillModelAny::default();
1536 assert!(matches!(model, FillModelAny::Default(_)));
1537 }
1538
1539 #[rstest]
1540 fn test_fill_model_any_is_limit_filled() {
1541 let mut model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.1, Some(42)).unwrap());
1542 let result = model.is_limit_filled();
1543 assert!(!result);
1544 }
1545
1546 #[rstest]
1547 fn test_default_fill_model_fill_limit_inside_spread_is_false() {
1548 let model = DefaultFillModel::default();
1549 assert!(!model.fill_limit_inside_spread());
1550 }
1551
1552 #[rstest]
1553 fn test_best_price_fill_model_fill_limit_inside_spread_is_true() {
1554 let model = BestPriceFillModel::default();
1555 assert!(model.fill_limit_inside_spread());
1556 }
1557
1558 #[rstest]
1559 fn test_one_tick_slippage_fill_model_fill_limit_inside_spread_is_false() {
1560 let model = OneTickSlippageFillModel::default();
1561 assert!(!model.fill_limit_inside_spread());
1562 }
1563
1564 #[rstest]
1565 fn test_fill_model_any_fill_limit_inside_spread_dispatch() {
1566 let default = FillModelAny::Default(DefaultFillModel::default());
1567 assert!(!default.fill_limit_inside_spread());
1568
1569 let best_price = FillModelAny::BestPrice(BestPriceFillModel::default());
1570 assert!(best_price.fill_limit_inside_spread());
1571
1572 let one_tick = FillModelAny::OneTickSlippage(OneTickSlippageFillModel::default());
1573 assert!(!one_tick.fill_limit_inside_spread());
1574 }
1575}