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