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